@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
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pet registry — the multi-pet contract. One pet is a directory holding a
|
|
3
|
+
* 'pet.json' manifest plus an atlas image; nothing else is required, and no
|
|
4
|
+
* host or client code changes when a pet is added. The registry scans three
|
|
5
|
+
* sources, later sources overriding earlier ones on an id collision:
|
|
6
|
+
*
|
|
7
|
+
* 1. the package's own 'assets' subdirectories (built-in pets);
|
|
8
|
+
* 2. '${CODEX_HOME:-~/.codex}/pets' subdirectories (hatch-pet custom pets);
|
|
9
|
+
* 3. 'PetConfig.pets' manifests composed by the embedding application
|
|
10
|
+
* (highest precedence).
|
|
11
|
+
*
|
|
12
|
+
* The manifest follows the Codex/hatch-pet contract (8 columns x 9 rows of
|
|
13
|
+
* 192x208 cells, the 9-state row order below). Legacy whale-girl manifests
|
|
14
|
+
* that only carry 'frames' keep working: geometry, per-row frame counts and
|
|
15
|
+
* per-track rhythm all fall back to the hatch-pet contract defaults, and the
|
|
16
|
+
* whale-girl manifest overrides its own durations.
|
|
17
|
+
* @module @linxin666/dsh-pet/registry
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { homedir } from 'node:os';
|
|
21
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
/** Fixed row order of the 9-state animation contract. */
|
|
24
|
+
export const PET_ROW_ORDER = [
|
|
25
|
+
'idle',
|
|
26
|
+
'running-right',
|
|
27
|
+
'running-left',
|
|
28
|
+
'waving',
|
|
29
|
+
'jumping',
|
|
30
|
+
'failed',
|
|
31
|
+
'waiting',
|
|
32
|
+
'running',
|
|
33
|
+
'review',
|
|
34
|
+
];
|
|
35
|
+
/** Atlas cell size in px (Codex/hatch-pet contract). */
|
|
36
|
+
export const DEFAULT_PET_CELL = { width: 192, height: 208 };
|
|
37
|
+
/** Columns per row (max frames per track). */
|
|
38
|
+
export const DEFAULT_PET_COLUMNS = 8;
|
|
39
|
+
/** Rows in the atlas (fixed by the animation contract). */
|
|
40
|
+
export const DEFAULT_PET_ROW_COUNT = 9;
|
|
41
|
+
/**
|
|
42
|
+
* Per-row used-column counts from the hatch-pet contract table. Manifests
|
|
43
|
+
* that carry no 'frames' field (the Codex custom-pet shape) resolve here.
|
|
44
|
+
*/
|
|
45
|
+
export const DEFAULT_FRAME_COUNTS = [6, 8, 8, 4, 5, 8, 6, 6, 6];
|
|
46
|
+
/** Absolute package root, resolved from a module URL (lib/ or src/). */
|
|
47
|
+
export function petPackageRoot(importMetaUrl) {
|
|
48
|
+
return fileURLToPath(new URL('../', importMetaUrl));
|
|
49
|
+
}
|
|
50
|
+
/** Resolve the hatch-pet custom pets directory (CODEX_HOME or ~/.codex). */
|
|
51
|
+
export function codexPetsDir(env = process.env, home = homedir()) {
|
|
52
|
+
const raw = env.CODEX_HOME !== undefined && env.CODEX_HOME.trim() !== ''
|
|
53
|
+
? env.CODEX_HOME.trim()
|
|
54
|
+
: join(home, '.codex');
|
|
55
|
+
const expanded = raw === '~'
|
|
56
|
+
? home
|
|
57
|
+
: (raw.startsWith('~/') || raw.startsWith('~\\')) ? join(home, raw.slice(2)) : raw;
|
|
58
|
+
return join(expanded, 'pets');
|
|
59
|
+
}
|
|
60
|
+
/** Finite non-negative integer guard, else the fallback. */
|
|
61
|
+
function finiteInt(value, fallback, max) {
|
|
62
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= max
|
|
63
|
+
? value
|
|
64
|
+
: fallback;
|
|
65
|
+
}
|
|
66
|
+
/** Build the browser URL of one pet asset. */
|
|
67
|
+
function assetUrl(prefix, id, file) {
|
|
68
|
+
const path = file.split('/').filter(segment => segment !== '').join('/');
|
|
69
|
+
return prefix + '/' + encodeURIComponent(id) + '/' + path;
|
|
70
|
+
}
|
|
71
|
+
/** Default per-track rhythm (hatch-pet contract table). */
|
|
72
|
+
export const DEFAULT_TRACK_PATTERNS = {
|
|
73
|
+
idle: { durations: [280, 110, 110, 140, 140, 320], loop: true },
|
|
74
|
+
'running-right': { durations: [120, 120, 120, 120, 120, 120, 120, 220], loop: true },
|
|
75
|
+
'running-left': { durations: [120, 120, 120, 120, 120, 120, 120, 220], loop: true },
|
|
76
|
+
waving: { durations: [140, 140, 140, 280], loop: true },
|
|
77
|
+
jumping: { durations: [140, 140, 140, 140, 280], loop: false, fallback: 'idle' },
|
|
78
|
+
failed: { durations: [140, 140, 140, 140, 140, 140, 140, 240], loop: false, fallback: 'idle' },
|
|
79
|
+
waiting: { durations: [150, 150, 150, 150, 150, 260], loop: true },
|
|
80
|
+
running: { durations: [120, 120, 120, 120, 120, 220], loop: true },
|
|
81
|
+
review: { durations: [150, 150, 150, 150, 150, 280], loop: true },
|
|
82
|
+
};
|
|
83
|
+
/** Stable id charset: keeps asset URLs plain and filesystem-safe. */
|
|
84
|
+
const PET_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
85
|
+
/** Safe path-segment charset for atlas files. */
|
|
86
|
+
const PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
87
|
+
const PET_NAME_MAX_LENGTH = 80;
|
|
88
|
+
/**
|
|
89
|
+
* Normalize one parsed manifest into a renderable pet entry, or undefined
|
|
90
|
+
* (with a warning recorded) when the manifest violates the contract.
|
|
91
|
+
*/
|
|
92
|
+
export function resolvePetManifest(raw, dir, options = {}) {
|
|
93
|
+
const { assetPrefix = '/pet', warnings = [] } = options;
|
|
94
|
+
const warn = (message) => { warnings.push(message); };
|
|
95
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
96
|
+
warn('manifest is not an object');
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const source = raw;
|
|
100
|
+
const id = typeof source.id === 'string' ? source.id.trim() : '';
|
|
101
|
+
if (!PET_ID_PATTERN.test(id)) {
|
|
102
|
+
warn('manifest id ' + JSON.stringify(String(source.id)) + ' is not a lowercase kebab id');
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const displayName = typeof source.displayName === 'string' && source.displayName.trim() !== ''
|
|
106
|
+
? source.displayName.trim().slice(0, PET_NAME_MAX_LENGTH)
|
|
107
|
+
: id;
|
|
108
|
+
const description = typeof source.description === 'string'
|
|
109
|
+
? source.description.trim()
|
|
110
|
+
: '';
|
|
111
|
+
const spritesheet = typeof source.spritesheetPath === 'string' && source.spritesheetPath.trim() !== ''
|
|
112
|
+
? source.spritesheetPath.trim()
|
|
113
|
+
: 'spritesheet.webp';
|
|
114
|
+
const spritesheetPath = spritesheet.split('/').filter(segment => segment !== '');
|
|
115
|
+
if (spritesheetPath.length === 0
|
|
116
|
+
|| isAbsolute(spritesheet)
|
|
117
|
+
|| spritesheet.includes('\\')
|
|
118
|
+
|| spritesheetPath.some(segment => segment === '..' || !PATH_SEGMENT_PATTERN.test(segment))) {
|
|
119
|
+
warn('manifest spritesheetPath ' + JSON.stringify(spritesheet) + ' is not a safe relative path');
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
const rawCell = (typeof source.cell === 'object' && source.cell !== null ? source.cell : {});
|
|
123
|
+
const cell = {
|
|
124
|
+
width: finiteInt(rawCell.width, DEFAULT_PET_CELL.width, 2048),
|
|
125
|
+
height: finiteInt(rawCell.height, DEFAULT_PET_CELL.height, 2048),
|
|
126
|
+
};
|
|
127
|
+
const columns = finiteInt(source.columns, DEFAULT_PET_COLUMNS, 32);
|
|
128
|
+
const rows = DEFAULT_FRAME_COUNTS.map((fallback, index) => {
|
|
129
|
+
const value = Array.isArray(source.frames) ? source.frames[index] : undefined;
|
|
130
|
+
return finiteInt(value, fallback, columns);
|
|
131
|
+
});
|
|
132
|
+
const trackOverrides = (typeof source.tracks === 'object' && source.tracks !== null ? source.tracks : {});
|
|
133
|
+
const tracks = {};
|
|
134
|
+
for (const [row, animation] of PET_ROW_ORDER.entries()) {
|
|
135
|
+
const pattern = DEFAULT_TRACK_PATTERNS[animation];
|
|
136
|
+
const override = trackOverrides[animation];
|
|
137
|
+
const durations = Array.isArray(override?.durations) && override.durations.length > 0
|
|
138
|
+
? override.durations.filter((value) => typeof value === 'number' && Number.isFinite(value) && value > 0)
|
|
139
|
+
: pattern.durations;
|
|
140
|
+
if (durations.length === 0) {
|
|
141
|
+
warn('manifest ' + id + ': track ' + animation + ' carries no usable durations');
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
const frameCount = Math.max(1, Math.min(rows[row], columns));
|
|
145
|
+
const sized = durations.length >= frameCount
|
|
146
|
+
? durations.slice(0, frameCount)
|
|
147
|
+
: Array.from({ length: frameCount }, (_, index) => durations[index % durations.length]);
|
|
148
|
+
tracks[animation] = {
|
|
149
|
+
frames: Array.from({ length: frameCount }, (_, index) => index),
|
|
150
|
+
durations: sized,
|
|
151
|
+
loop: typeof override?.loop === 'boolean' ? override.loop : pattern.loop,
|
|
152
|
+
...(override?.fallback === undefined
|
|
153
|
+
? pattern.fallback === undefined ? {} : { fallback: pattern.fallback }
|
|
154
|
+
: PET_ROW_ORDER.includes(override.fallback)
|
|
155
|
+
? { fallback: override.fallback }
|
|
156
|
+
: pattern.fallback === undefined ? {} : { fallback: pattern.fallback }),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
id,
|
|
161
|
+
displayName,
|
|
162
|
+
description,
|
|
163
|
+
cell,
|
|
164
|
+
columns,
|
|
165
|
+
rows,
|
|
166
|
+
tracks,
|
|
167
|
+
atlasUrl: assetUrl(assetPrefix, id, spritesheet),
|
|
168
|
+
manifestUrl: assetUrl(assetPrefix, id, 'pet.json'),
|
|
169
|
+
dir,
|
|
170
|
+
spritesheetPath: spritesheetPath.join('/'),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** Scan one directory of pet folders; entries come back in name order. */
|
|
174
|
+
function scanPetDir(dir, options) {
|
|
175
|
+
if (!existsSync(dir))
|
|
176
|
+
return [];
|
|
177
|
+
let names = [];
|
|
178
|
+
try {
|
|
179
|
+
names = readdirSync(dir).filter(name => !name.startsWith('.'));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
names.sort();
|
|
185
|
+
const entries = [];
|
|
186
|
+
for (const name of names) {
|
|
187
|
+
const manifestFile = join(dir, name, 'pet.json');
|
|
188
|
+
if (!existsSync(manifestFile))
|
|
189
|
+
continue;
|
|
190
|
+
const parsed = readPetJson(manifestFile, options.warnings);
|
|
191
|
+
if (parsed === undefined)
|
|
192
|
+
continue;
|
|
193
|
+
const entry = resolvePetManifest(parsed, join(dir, name), options);
|
|
194
|
+
if (entry !== undefined)
|
|
195
|
+
entries.push(entry);
|
|
196
|
+
}
|
|
197
|
+
return entries;
|
|
198
|
+
}
|
|
199
|
+
/** Read and parse one manifest file; undefined (warning recorded) on failure. */
|
|
200
|
+
function readPetJson(file, warnings) {
|
|
201
|
+
try {
|
|
202
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
warnings?.push('skipping ' + file + ': ' + (error instanceof Error ? error.message : String(error)));
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Load the pet registry: built-in 'assets/*' first, then the hatch-pet
|
|
211
|
+
* custom pets directory, then composed 'extra' manifests (each later source
|
|
212
|
+
* overrides an earlier one on id collision). The registry never throws on a
|
|
213
|
+
* bad manifest: it skips it and records a warning.
|
|
214
|
+
*/
|
|
215
|
+
export function loadPetRegistry(options) {
|
|
216
|
+
const { packageRoot, assetPrefix = '/pet' } = options;
|
|
217
|
+
const warnings = [];
|
|
218
|
+
const byId = new Map();
|
|
219
|
+
const builtinIds = new Set();
|
|
220
|
+
for (const entry of scanPetDir(join(packageRoot, 'assets'), { assetPrefix, warnings })) {
|
|
221
|
+
if (byId.has(entry.id)) {
|
|
222
|
+
warnings.push('duplicate built-in pet id ' + entry.id + '; the first one wins');
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
byId.set(entry.id, entry);
|
|
226
|
+
builtinIds.add(entry.id);
|
|
227
|
+
}
|
|
228
|
+
const petsDir = options.petsDir ?? codexPetsDir();
|
|
229
|
+
if (petsDir !== '') {
|
|
230
|
+
for (const entry of scanPetDir(petsDir, { assetPrefix, warnings })) {
|
|
231
|
+
if (byId.has(entry.id))
|
|
232
|
+
warnings.push('custom pet ' + entry.id + ' overrides the built-in one');
|
|
233
|
+
byId.set(entry.id, entry);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
for (const manifest of options.extra ?? []) {
|
|
237
|
+
const dir = manifest.spritesheetPath === undefined || isAbsolute(manifest.spritesheetPath)
|
|
238
|
+
? join(packageRoot, 'assets', 'extra')
|
|
239
|
+
: dirname(resolve(packageRoot, manifest.spritesheetPath));
|
|
240
|
+
const entry = resolvePetManifest(manifest, dir, { assetPrefix, warnings });
|
|
241
|
+
if (entry === undefined)
|
|
242
|
+
continue;
|
|
243
|
+
if (byId.has(entry.id))
|
|
244
|
+
warnings.push('composed pet ' + entry.id + ' overrides an earlier registration');
|
|
245
|
+
byId.set(entry.id, entry);
|
|
246
|
+
}
|
|
247
|
+
const entries = [...byId.values()];
|
|
248
|
+
return {
|
|
249
|
+
entries,
|
|
250
|
+
warnings,
|
|
251
|
+
byId: (id) => byId.get(id),
|
|
252
|
+
defaultEntry: () => entries.find(entry => builtinIds.has(entry.id)) ?? entries[0],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/** Strip host-only fields, leaving the client-visible definition. */
|
|
256
|
+
export function petEntryView(entry) {
|
|
257
|
+
return {
|
|
258
|
+
id: entry.id,
|
|
259
|
+
displayName: entry.displayName,
|
|
260
|
+
description: entry.description,
|
|
261
|
+
cell: entry.cell,
|
|
262
|
+
columns: entry.columns,
|
|
263
|
+
rows: entry.rows,
|
|
264
|
+
tracks: entry.tracks,
|
|
265
|
+
atlasUrl: entry.atlasUrl,
|
|
266
|
+
manifestUrl: entry.manifestUrl,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/** The absolute file a pet's atlas resolves to (host asset route). */
|
|
270
|
+
export function petAtlasFile(entry) {
|
|
271
|
+
return join(entry.dir, entry.spritesheetPath);
|
|
272
|
+
}
|
|
273
|
+
/** The directory basename of one entry (legacy asset URL alias, e.g. whale). */
|
|
274
|
+
export function petDirAlias(entry) {
|
|
275
|
+
return basename(entry.dir);
|
|
276
|
+
}
|
package/lib/types/routes.d.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pet HTTP routes — the browser half talks to the host through plain
|
|
3
|
-
* same-origin JSON endpoints (
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* same-origin JSON endpoints ('/api/pet/*') and loads pet assets from
|
|
4
|
+
* '/pet/<id>/*'. The '/plugins/' endpoint only serves client bundles and RPC
|
|
5
|
+
* domains are platform-registered, so the pet serves its own API and media —
|
|
6
|
+
* the same pattern as dsh-remote-web-ui's '/api/pair' family. The asset route
|
|
7
|
+
* is one prefix registration serving every registry entry (manifest, atlas,
|
|
8
|
+
* optional previews), so adding a pet never touches route wiring.
|
|
7
9
|
* @module @linxin666/dsh-pet/routes
|
|
8
10
|
*/
|
|
9
11
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
10
12
|
import type { PetService } from './service.ts';
|
|
11
13
|
/** Browser-facing base path of the pet API. */
|
|
12
14
|
export declare const PET_API_PREFIX = "/api/pet";
|
|
13
|
-
/** Browser-facing base path of the pet asset routes. */
|
|
14
|
-
export declare const PET_ASSET_PREFIX = "/pet
|
|
15
|
-
/**
|
|
16
|
-
export declare function petPackageRoot(importMetaUrl: string): string;
|
|
17
|
-
/** Build the full route family (API + assets) for one service + package root. */
|
|
15
|
+
/** Browser-facing base path of the pet asset routes ('/pet/<id>/...'). */
|
|
16
|
+
export declare const PET_ASSET_PREFIX = "/pet";
|
|
17
|
+
/** Build the full route family (API + assets) for one service. */
|
|
18
18
|
export declare function makePetRoutes(deps: {
|
|
19
19
|
service: PetService;
|
|
20
|
-
packageRoot: string;
|
|
21
20
|
}): WebRoute[];
|
|
21
|
+
export { petPackageRoot } from './registry.ts';
|
|
22
22
|
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAI9C,+CAA+C;AAC/C,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,SAAS,CAAA;AAuNtC,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,UAAU,CAAA;CAAE,GAAG,QAAQ,EAAE,CAwCvE;AAGD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA"}
|
package/lib/types/routes.js
CHANGED
|
@@ -1,26 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pet HTTP routes — the browser half talks to the host through plain
|
|
3
|
-
* same-origin JSON endpoints (
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* same-origin JSON endpoints ('/api/pet/*') and loads pet assets from
|
|
4
|
+
* '/pet/<id>/*'. The '/plugins/' endpoint only serves client bundles and RPC
|
|
5
|
+
* domains are platform-registered, so the pet serves its own API and media —
|
|
6
|
+
* the same pattern as dsh-remote-web-ui's '/api/pair' family. The asset route
|
|
7
|
+
* is one prefix registration serving every registry entry (manifest, atlas,
|
|
8
|
+
* optional previews), so adding a pet never touches route wiring.
|
|
7
9
|
* @module @linxin666/dsh-pet/routes
|
|
8
10
|
*/
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
9
12
|
import { readFile } from 'node:fs/promises';
|
|
10
13
|
import { join } from 'node:path';
|
|
11
|
-
import {
|
|
14
|
+
import { petEntryView } from "./registry.js";
|
|
12
15
|
/** Browser-facing base path of the pet API. */
|
|
13
16
|
export const PET_API_PREFIX = '/api/pet';
|
|
14
|
-
/** Browser-facing base path of the pet asset routes. */
|
|
15
|
-
export const PET_ASSET_PREFIX = '/pet
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
/** Browser-facing base path of the pet asset routes ('/pet/<id>/...'). */
|
|
18
|
+
export const PET_ASSET_PREFIX = '/pet';
|
|
19
|
+
const MANIFEST_FILE = 'pet.json';
|
|
20
|
+
const PREVIEW_DIR = 'previews';
|
|
21
|
+
const PREVIEW_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
22
|
+
const MIME_BY_EXT = {
|
|
23
|
+
'.webp': 'image/webp',
|
|
24
|
+
'.png': 'image/png',
|
|
25
|
+
'.gif': 'image/gif',
|
|
26
|
+
'.jpg': 'image/jpeg',
|
|
27
|
+
'.jpeg': 'image/jpeg',
|
|
28
|
+
'.json': 'application/json',
|
|
29
|
+
};
|
|
30
|
+
/** Content type by file extension (safe fallback: octet-stream). */
|
|
31
|
+
function mimeFor(file) {
|
|
32
|
+
const dot = file.lastIndexOf('.');
|
|
33
|
+
if (dot < 0)
|
|
34
|
+
return 'application/octet-stream';
|
|
35
|
+
return MIME_BY_EXT[file.slice(dot).toLowerCase()] ?? 'application/octet-stream';
|
|
24
36
|
}
|
|
25
37
|
/** Write one JSON response. */
|
|
26
38
|
function json(res, status, body) {
|
|
@@ -42,8 +54,6 @@ function readJsonBody(req) {
|
|
|
42
54
|
req.on('data', (chunk) => {
|
|
43
55
|
size += chunk.length;
|
|
44
56
|
if (size > 64 * 1024) {
|
|
45
|
-
// Reject first so the error handler can write the 400 response,
|
|
46
|
-
// then close the connection once the response is flushed.
|
|
47
57
|
reject(new Error('body-too-large'));
|
|
48
58
|
queueMicrotask(() => req.destroy());
|
|
49
59
|
return;
|
|
@@ -98,61 +108,169 @@ function postRoute(path, run) {
|
|
|
98
108
|
},
|
|
99
109
|
};
|
|
100
110
|
}
|
|
101
|
-
/**
|
|
111
|
+
/** Legacy URL aliases: each entry's directory basename (e.g. 'whale'). */
|
|
112
|
+
function dirAliases(registry) {
|
|
113
|
+
const aliases = new Map();
|
|
114
|
+
for (const entry of registry.entries) {
|
|
115
|
+
const alias = entry.dir.split(/[\\/]/).pop() ?? '';
|
|
116
|
+
if (alias !== '' && !aliases.has(alias))
|
|
117
|
+
aliases.set(alias, entry);
|
|
118
|
+
}
|
|
119
|
+
return aliases;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The one asset handler behind the '/pet' prefix. Resolves the pet by id (or
|
|
123
|
+
* legacy directory alias), then serves exactly the files a manifest declares:
|
|
124
|
+
* pet.json, the declared spritesheet path, and optional 'previews/<name>'
|
|
125
|
+
* media. Composed pets without a manifest file get a synthesized pet.json.
|
|
126
|
+
*/
|
|
127
|
+
function assetHandler(registry) {
|
|
128
|
+
const aliases = dirAliases(registry);
|
|
129
|
+
return (req, res) => {
|
|
130
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
131
|
+
res.writeHead(405);
|
|
132
|
+
res.end();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
let pathname;
|
|
136
|
+
try {
|
|
137
|
+
pathname = new URL(req.url ?? '/', 'http://pet.local').pathname;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
res.writeHead(400);
|
|
141
|
+
res.end();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const segments = pathname.split('/').filter(segment => segment !== '');
|
|
145
|
+
if (segments[0] !== 'pet' || segments[1] === undefined) {
|
|
146
|
+
res.writeHead(404);
|
|
147
|
+
res.end();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
let id;
|
|
151
|
+
try {
|
|
152
|
+
id = decodeURIComponent(segments[1]);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
res.writeHead(400);
|
|
156
|
+
res.end();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const entry = registry.byId(id) ?? aliases.get(id);
|
|
160
|
+
if (entry === undefined) {
|
|
161
|
+
res.writeHead(404);
|
|
162
|
+
res.end();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const rest = [];
|
|
166
|
+
for (const segment of segments.slice(2)) {
|
|
167
|
+
let decoded;
|
|
168
|
+
try {
|
|
169
|
+
decoded = decodeURIComponent(segment);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
res.writeHead(400);
|
|
173
|
+
res.end();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
rest.push(decoded);
|
|
177
|
+
}
|
|
178
|
+
const rel = rest.join('/');
|
|
179
|
+
let file;
|
|
180
|
+
let synthesized = false;
|
|
181
|
+
if (rest.length === 1 && rest[0] === MANIFEST_FILE) {
|
|
182
|
+
const manifestFile = join(entry.dir, MANIFEST_FILE);
|
|
183
|
+
file = existsSync(manifestFile) ? manifestFile : undefined;
|
|
184
|
+
if (file === undefined)
|
|
185
|
+
synthesized = true;
|
|
186
|
+
}
|
|
187
|
+
else if (rest.length > 0 && rel === entry.spritesheetPath) {
|
|
188
|
+
file = join(entry.dir, entry.spritesheetPath);
|
|
189
|
+
}
|
|
190
|
+
else if (rest.length === 2 && rest[0] === PREVIEW_DIR && PREVIEW_PATTERN.test(rest[1])) {
|
|
191
|
+
const preview = join(entry.dir, PREVIEW_DIR, rest[1]);
|
|
192
|
+
file = existsSync(preview) ? preview : undefined;
|
|
193
|
+
}
|
|
194
|
+
if (synthesized) {
|
|
195
|
+
const body = Buffer.from(JSON.stringify(petEntryView(entry), null, 2), 'utf8');
|
|
196
|
+
res.writeHead(200, {
|
|
197
|
+
'content-type': 'application/json; charset=utf-8',
|
|
198
|
+
'content-length': String(body.byteLength),
|
|
199
|
+
'cache-control': 'no-cache',
|
|
200
|
+
});
|
|
201
|
+
if (req.method === 'HEAD') {
|
|
202
|
+
res.end();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
res.end(body);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (file === undefined) {
|
|
209
|
+
res.writeHead(404);
|
|
210
|
+
res.end();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const resolved = file;
|
|
214
|
+
readFile(resolved).then((body) => {
|
|
215
|
+
res.writeHead(200, {
|
|
216
|
+
'content-type': mimeFor(resolved),
|
|
217
|
+
'content-length': String(body.byteLength),
|
|
218
|
+
'cache-control': 'no-cache',
|
|
219
|
+
});
|
|
220
|
+
if (req.method === 'HEAD') {
|
|
221
|
+
res.end();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
res.end(body);
|
|
225
|
+
}, () => {
|
|
226
|
+
res.writeHead(404);
|
|
227
|
+
res.end();
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** Build the full route family (API + assets) for one service. */
|
|
102
232
|
export function makePetRoutes(deps) {
|
|
103
|
-
const { service
|
|
233
|
+
const { service } = deps;
|
|
104
234
|
const apiRoutes = [
|
|
105
|
-
getRoute(
|
|
106
|
-
|
|
235
|
+
getRoute(PET_API_PREFIX + '/state', () => service.state()),
|
|
236
|
+
getRoute(PET_API_PREFIX + '/pets', () => service.pets()),
|
|
237
|
+
postRoute(PET_API_PREFIX + '/interact', (body) => {
|
|
107
238
|
const kind = body.kind;
|
|
108
239
|
if (kind !== 'pet' && kind !== 'feed')
|
|
109
240
|
return Promise.reject(new Error('invalid-kind'));
|
|
110
241
|
return service.interact(kind);
|
|
111
242
|
}),
|
|
112
|
-
postRoute(
|
|
243
|
+
postRoute(PET_API_PREFIX + '/set-visible', (body) => {
|
|
113
244
|
const visible = body.visible;
|
|
114
245
|
if (typeof visible !== 'boolean')
|
|
115
246
|
return Promise.reject(new Error('invalid-visible'));
|
|
116
247
|
return service.setVisible(visible);
|
|
117
248
|
}),
|
|
118
|
-
postRoute(
|
|
249
|
+
postRoute(PET_API_PREFIX + '/set-config', (body) => service.setConfig({
|
|
119
250
|
...(typeof body.size === 'number' ? { size: body.size } : {}),
|
|
120
251
|
...(typeof body.right === 'number' ? { right: body.right } : {}),
|
|
121
252
|
...(typeof body.bottom === 'number' ? { bottom: body.bottom } : {}),
|
|
122
253
|
...(typeof body.visible === 'boolean' ? { visible: body.visible } : {}),
|
|
123
254
|
})),
|
|
124
|
-
postRoute(
|
|
255
|
+
postRoute(PET_API_PREFIX + '/set-name', (body) => {
|
|
125
256
|
const name = body.name;
|
|
126
257
|
if (typeof name !== 'string')
|
|
127
258
|
return Promise.reject(new Error('invalid-name'));
|
|
128
259
|
return service.setName(name);
|
|
129
260
|
}),
|
|
261
|
+
postRoute(PET_API_PREFIX + '/set-pet', (body) => {
|
|
262
|
+
const petId = body.petId;
|
|
263
|
+
if (typeof petId !== 'string')
|
|
264
|
+
return Promise.reject(new Error('invalid-pet'));
|
|
265
|
+
return service.setPetId(petId);
|
|
266
|
+
}),
|
|
130
267
|
];
|
|
131
|
-
const
|
|
132
|
-
kind: '
|
|
133
|
-
path:
|
|
134
|
-
handler: (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
res.end();
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
return readFile(join(packageRoot, 'assets', 'whale', file.name)).then((body) => {
|
|
141
|
-
res.writeHead(200, {
|
|
142
|
-
'content-type': file.mime,
|
|
143
|
-
'content-length': String(body.byteLength),
|
|
144
|
-
'cache-control': 'no-cache',
|
|
145
|
-
});
|
|
146
|
-
if (req.method === 'HEAD') {
|
|
147
|
-
res.end();
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
res.end(body);
|
|
151
|
-
}, () => {
|
|
152
|
-
res.writeHead(404);
|
|
153
|
-
res.end();
|
|
154
|
-
});
|
|
155
|
-
},
|
|
156
|
-
}));
|
|
157
|
-
return [...apiRoutes, ...assetRoutes];
|
|
268
|
+
const assetRoute = {
|
|
269
|
+
kind: 'prefix',
|
|
270
|
+
path: PET_ASSET_PREFIX,
|
|
271
|
+
handler: assetHandler(service.registrySnapshot()),
|
|
272
|
+
};
|
|
273
|
+
return [...apiRoutes, assetRoute];
|
|
158
274
|
}
|
|
275
|
+
// Re-exported for the package surface (the registry owns the definition now).
|
|
276
|
+
export { petPackageRoot } from "./registry.js";
|