@linxin666/dsh-pet 0.2.2 → 0.2.3
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 +66 -3
- package/README.zh.md +66 -3
- package/assets/whale/pet.json +169 -20
- package/assets/whale-refined/pet.json +17 -2
- package/lib/client.js +438 -21
- package/lib/client.js.map +1 -1
- package/lib/index.js +752 -36
- package/lib/live2d-vendor.js +995 -0
- package/lib/live2d-vendor.js.map +1 -0
- 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 +10 -0
- package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PetSettingsCard.js +22 -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 +17 -8
- 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 +10 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +10 -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 +180 -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/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/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 +57 -3
- package/lib/types/registry.d.ts.map +1 -1
- package/lib/types/registry.js +208 -33
- package/lib/types/routes.d.ts +39 -2
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +183 -12
- package/lib/types/service.d.ts +5 -1
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +4 -0
- package/package.json +3 -1
- package/src/client/PetDockEntry.tsx +19 -11
- package/src/client/PetSettingsCard.tsx +39 -0
- package/src/client/PetSprite.test.tsx +11 -0
- package/src/client/PetSprite.tsx +24 -9
- package/src/client/index.ts +6 -0
- package/src/client/locales.ts +10 -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 +242 -0
- package/src/client/renderers/live2d.ts +218 -0
- package/src/client/renderers/registry.ts +58 -0
- package/src/client/settings-section.module.css +23 -0
- package/src/contracts/renderer.ts +54 -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 +215 -2
- package/src/registry.ts +246 -34
- package/src/routes.ts +202 -13
- package/src/service.ts +6 -0
package/src/registry.ts
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pet registry — the multi-pet contract. One pet is a directory holding a
|
|
3
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
|
|
4
|
+
* host or client code changes when a pet is added. The registry scans four
|
|
5
5
|
* sources, later sources overriding earlier ones on an id collision:
|
|
6
6
|
*
|
|
7
7
|
* 1. the package's own 'assets' subdirectories (built-in pets);
|
|
8
|
-
* 2. '${CODEX_HOME:-~/.codex}/pets' subdirectories (hatch-pet custom pets
|
|
9
|
-
*
|
|
8
|
+
* 2. '${CODEX_HOME:-~/.codex}/pets' subdirectories (hatch-pet custom pets,
|
|
9
|
+
* legacy source kept readable);
|
|
10
|
+
* 3. '$DSH_HOME/pets' subdirectories (the pet-center user directory);
|
|
11
|
+
* 4. 'PetConfig.pets' manifests composed by the embedding application
|
|
10
12
|
* (highest precedence).
|
|
11
13
|
*
|
|
14
|
+
* Manifests are parsed through manifest-v2 (pet-center M2, issue #623): v1
|
|
15
|
+
* manifests are compat-read as sprite2d, v2 manifests validate fail-closed,
|
|
16
|
+
* and structured diagnostics ride alongside the legacy warnings. Live2d
|
|
17
|
+
* entries (pet-center M3) list like any other pet: the entry carries the
|
|
18
|
+
* validated live2d block plus the model's reference closure (the servable
|
|
19
|
+
* set the asset route allows), and a model3.json that is unreadable or
|
|
20
|
+
* declares unsafe references rejects the entry with an error diagnostic.
|
|
21
|
+
*
|
|
12
22
|
* The manifest follows the Codex/hatch-pet contract (8 columns x 9 rows of
|
|
13
23
|
* 192x208 cells, the 9-state row order below). Legacy whale-girl manifests
|
|
14
24
|
* that only carry 'frames' keep working: geometry, per-row frame counts and
|
|
@@ -23,6 +33,9 @@ import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
|
|
|
23
33
|
import { fileURLToPath } from 'node:url'
|
|
24
34
|
import type { ActivityPhase, PetAnimation } from './state.ts'
|
|
25
35
|
import { normalizePetRemarks, type PetRemarks, type PetRemarksManifest } from './remarks.ts'
|
|
36
|
+
import { dshHome } from './dsh-home.ts'
|
|
37
|
+
import { parsePetManifest, type PetManifestLive2d, type PetManifestV2, type PetRendererKind } from './manifest-v2.ts'
|
|
38
|
+
import { collectModel3References } from './model3.ts'
|
|
26
39
|
|
|
27
40
|
/** Fixed row order of the 9-state animation contract. */
|
|
28
41
|
export const PET_ROW_ORDER: readonly PetAnimation[] = [
|
|
@@ -156,11 +169,33 @@ export interface PetTrackOverride {
|
|
|
156
169
|
fallback?: PetAnimation
|
|
157
170
|
}
|
|
158
171
|
|
|
172
|
+
/** The live2d renderer block as served to the browser half (pet-center M3). */
|
|
173
|
+
export interface PetLive2dDefinition {
|
|
174
|
+
/** Browser URL of the .model3.json (served by the host asset route). */
|
|
175
|
+
modelUrl: string
|
|
176
|
+
/** Manifest-relative model path (host route allow-list key). */
|
|
177
|
+
modelPath: string
|
|
178
|
+
/** Scale multiplier over the canvas auto-fit, (0, 10]. */
|
|
179
|
+
scale?: number
|
|
180
|
+
/** Model offset in canvas px from the center-bottom anchor. */
|
|
181
|
+
translate?: { x?: number; y?: number }
|
|
182
|
+
/** ActivityPhase -> motion group; unmapped phases fall back to idle. */
|
|
183
|
+
motions: Partial<Record<ActivityPhase, string>> & { idle: string }
|
|
184
|
+
/** Optional ActivityPhase -> expression name layered over the motion. */
|
|
185
|
+
expressions?: Partial<Record<ActivityPhase, string>>
|
|
186
|
+
/** Hit area names triggering the tap motion; defaults to every model HitArea. */
|
|
187
|
+
hitAreas?: string[]
|
|
188
|
+
}
|
|
189
|
+
|
|
159
190
|
/** A normalized pet as served to the browser half. */
|
|
160
191
|
export interface PetDefinition {
|
|
161
192
|
id: string
|
|
162
193
|
displayName: string
|
|
163
194
|
description: string
|
|
195
|
+
/** The renderer this entry mounts with (pet-center M2). */
|
|
196
|
+
renderer: PetRendererKind
|
|
197
|
+
/** Live2d render block; present exactly when renderer is 'live2d' (M3). */
|
|
198
|
+
live2d?: PetLive2dDefinition
|
|
164
199
|
/** Atlas cell size in px. */
|
|
165
200
|
cell: PetCell
|
|
166
201
|
/** Columns per row. */
|
|
@@ -185,6 +220,12 @@ export interface PetEntry extends PetDefinition {
|
|
|
185
220
|
dir: string
|
|
186
221
|
/** Atlas path relative to 'dir' (declared by the manifest). */
|
|
187
222
|
spritesheetPath: string
|
|
223
|
+
/**
|
|
224
|
+
* Manifest-relative files the asset route may serve beyond pet.json and
|
|
225
|
+
* 'previews/*' (pet-center M3): the sprite2d atlas, or the live2d model
|
|
226
|
+
* plus its model3.json reference closure.
|
|
227
|
+
*/
|
|
228
|
+
servable: readonly string[]
|
|
188
229
|
/** Normalized per-pet remark pools (manifest 'remarks'), when declared. */
|
|
189
230
|
remarks?: PetRemarks
|
|
190
231
|
}
|
|
@@ -193,11 +234,21 @@ export interface PetEntry extends PetDefinition {
|
|
|
193
234
|
export interface PetRegistry {
|
|
194
235
|
entries: PetEntry[]
|
|
195
236
|
warnings: string[]
|
|
237
|
+
/** Structured diagnostics from the manifest-v2 parse (superset detail of warnings). */
|
|
238
|
+
diagnostics: PetRegistryDiagnostic[]
|
|
196
239
|
byId(id: string): PetEntry | undefined
|
|
197
240
|
/** The pet an installation falls back to when the selection is unknown. */
|
|
198
241
|
defaultEntry(): PetEntry
|
|
199
242
|
}
|
|
200
243
|
|
|
244
|
+
/** One structured registry diagnostic (manifest-v2 era). */
|
|
245
|
+
export interface PetRegistryDiagnostic {
|
|
246
|
+
level: 'error' | 'warning'
|
|
247
|
+
/** Where the diagnostic originates (directory or file). */
|
|
248
|
+
source: string
|
|
249
|
+
message: string
|
|
250
|
+
}
|
|
251
|
+
|
|
201
252
|
/** Registry sources. */
|
|
202
253
|
export interface PetRegistryOptions {
|
|
203
254
|
/** Absolute package root whose 'assets/*' hold built-in pets. */
|
|
@@ -206,6 +257,8 @@ export interface PetRegistryOptions {
|
|
|
206
257
|
assetPrefix?: string
|
|
207
258
|
/** Custom pet directory (defaults to '${CODEX_HOME:-~/.codex}/pets'). */
|
|
208
259
|
petsDir?: string
|
|
260
|
+
/** Pet-center user directory (defaults to '$DSH_HOME/pets'; '' disables). */
|
|
261
|
+
dshPetsDir?: string
|
|
209
262
|
/** Extra manifest entries composed by the embedding application. */
|
|
210
263
|
extra?: readonly PetManifest[]
|
|
211
264
|
}
|
|
@@ -249,6 +302,47 @@ function normalizeSequences(
|
|
|
249
302
|
return Object.keys(sequences).length === 0 ? undefined : sequences
|
|
250
303
|
}
|
|
251
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Build the fully resolved animation tracks from the contract defaults plus
|
|
307
|
+
* optional per-track overrides. Shared by the sprite2d resolver and the
|
|
308
|
+
* live2d entry builder (which fills the sprite fields with contract
|
|
309
|
+
* defaults so the flat PetDefinition shape holds for every renderer).
|
|
310
|
+
*/
|
|
311
|
+
function buildTracks(
|
|
312
|
+
rows: readonly number[],
|
|
313
|
+
columns: number,
|
|
314
|
+
trackOverrides: Partial<Record<PetAnimation, PetTrackOverride>>,
|
|
315
|
+
warn: (message: string) => void,
|
|
316
|
+
): Record<PetAnimation, PetTrackDef> | undefined {
|
|
317
|
+
const tracks = {} as Record<PetAnimation, PetTrackDef>
|
|
318
|
+
for (const [row, animation] of PET_ROW_ORDER.entries()) {
|
|
319
|
+
const pattern = DEFAULT_TRACK_PATTERNS[animation]
|
|
320
|
+
const override = trackOverrides[animation]
|
|
321
|
+
const durations = Array.isArray(override?.durations) && override.durations.length > 0
|
|
322
|
+
? override.durations.filter((value): value is number => typeof value === 'number' && Number.isFinite(value) && value > 0)
|
|
323
|
+
: pattern.durations
|
|
324
|
+
if (durations.length === 0) {
|
|
325
|
+
warn('track ' + animation + ' carries no usable durations')
|
|
326
|
+
return undefined
|
|
327
|
+
}
|
|
328
|
+
const frameCount = Math.max(1, Math.min(rows[row]!, columns))
|
|
329
|
+
const sized = durations.length >= frameCount
|
|
330
|
+
? durations.slice(0, frameCount)
|
|
331
|
+
: Array.from({ length: frameCount }, (_, index) => durations[index % durations.length]!)
|
|
332
|
+
tracks[animation] = {
|
|
333
|
+
frames: Array.from({ length: frameCount }, (_, index) => index),
|
|
334
|
+
durations: sized,
|
|
335
|
+
loop: typeof override?.loop === 'boolean' ? override.loop : pattern.loop,
|
|
336
|
+
...(override?.fallback === undefined
|
|
337
|
+
? pattern.fallback === undefined ? {} : { fallback: pattern.fallback }
|
|
338
|
+
: PET_ROW_ORDER.includes(override.fallback)
|
|
339
|
+
? { fallback: override.fallback }
|
|
340
|
+
: pattern.fallback === undefined ? {} : { fallback: pattern.fallback }),
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return tracks
|
|
344
|
+
}
|
|
345
|
+
|
|
252
346
|
/**
|
|
253
347
|
* Normalize one parsed manifest into a renderable pet entry, or undefined
|
|
254
348
|
* (with a warning recorded) when the manifest violates the contract.
|
|
@@ -304,36 +398,14 @@ export function resolvePetManifest(
|
|
|
304
398
|
const remarks = normalizePetRemarks(source.remarks, message => warn('manifest ' + id + ': ' + message))
|
|
305
399
|
const sequences = normalizeSequences(source.sequences, id, warn)
|
|
306
400
|
const trackOverrides = (typeof source.tracks === 'object' && source.tracks !== null ? source.tracks : {}) as Partial<Record<PetAnimation, PetTrackOverride>>
|
|
307
|
-
const tracks =
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const override = trackOverrides[animation]
|
|
311
|
-
const durations = Array.isArray(override?.durations) && override.durations.length > 0
|
|
312
|
-
? override.durations.filter((value): value is number => typeof value === 'number' && Number.isFinite(value) && value > 0)
|
|
313
|
-
: pattern.durations
|
|
314
|
-
if (durations.length === 0) {
|
|
315
|
-
warn('manifest ' + id + ': track ' + animation + ' carries no usable durations')
|
|
316
|
-
return undefined
|
|
317
|
-
}
|
|
318
|
-
const frameCount = Math.max(1, Math.min(rows[row]!, columns))
|
|
319
|
-
const sized = durations.length >= frameCount
|
|
320
|
-
? durations.slice(0, frameCount)
|
|
321
|
-
: Array.from({ length: frameCount }, (_, index) => durations[index % durations.length]!)
|
|
322
|
-
tracks[animation] = {
|
|
323
|
-
frames: Array.from({ length: frameCount }, (_, index) => index),
|
|
324
|
-
durations: sized,
|
|
325
|
-
loop: typeof override?.loop === 'boolean' ? override.loop : pattern.loop,
|
|
326
|
-
...(override?.fallback === undefined
|
|
327
|
-
? pattern.fallback === undefined ? {} : { fallback: pattern.fallback }
|
|
328
|
-
: PET_ROW_ORDER.includes(override.fallback)
|
|
329
|
-
? { fallback: override.fallback }
|
|
330
|
-
: pattern.fallback === undefined ? {} : { fallback: pattern.fallback }),
|
|
331
|
-
}
|
|
332
|
-
}
|
|
401
|
+
const tracks = buildTracks(rows, columns, trackOverrides, message => warn('manifest ' + id + ': ' + message))
|
|
402
|
+
if (tracks === undefined) return undefined
|
|
403
|
+
const sheet = spritesheetPath.join('/')
|
|
333
404
|
return {
|
|
334
405
|
id,
|
|
335
406
|
displayName,
|
|
336
407
|
description,
|
|
408
|
+
renderer: 'sprite2d' as const,
|
|
337
409
|
cell,
|
|
338
410
|
columns,
|
|
339
411
|
rows,
|
|
@@ -343,13 +415,121 @@ export function resolvePetManifest(
|
|
|
343
415
|
atlasUrl: assetUrl(assetPrefix, id, spritesheet),
|
|
344
416
|
manifestUrl: assetUrl(assetPrefix, id, 'pet.json'),
|
|
345
417
|
dir,
|
|
346
|
-
spritesheetPath:
|
|
418
|
+
spritesheetPath: sheet,
|
|
419
|
+
servable: [sheet],
|
|
420
|
+
...(remarks === undefined ? {} : { remarks }),
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Adapt a validated v2 manifest's sprite2d block onto the legacy flat shape
|
|
426
|
+
* the established resolver consumes (pet-center M2 P2). The legacy resolver
|
|
427
|
+
* only expresses 9-row (default) and 11-row (spriteVersionNumber 2) atlases,
|
|
428
|
+
* so other atlasRows values are rejected here with a diagnostic.
|
|
429
|
+
*/
|
|
430
|
+
function flattenV2Sprite2d(manifest: PetManifestV2): Record<string, unknown> | undefined {
|
|
431
|
+
const block = manifest.sprite2d
|
|
432
|
+
if (block === undefined) return undefined
|
|
433
|
+
const legacy: Record<string, unknown> = {
|
|
434
|
+
id: manifest.id,
|
|
435
|
+
displayName: manifest.displayName,
|
|
436
|
+
spritesheetPath: block.spritesheetPath,
|
|
437
|
+
}
|
|
438
|
+
if (manifest.description !== undefined) legacy.description = manifest.description
|
|
439
|
+
if (block.cell !== undefined) legacy.cell = block.cell
|
|
440
|
+
if (block.columns !== undefined) legacy.columns = block.columns
|
|
441
|
+
if (block.frames !== undefined) legacy.frames = block.frames
|
|
442
|
+
if (block.tracks !== undefined) legacy.tracks = block.tracks
|
|
443
|
+
if (block.atlasRows !== undefined) {
|
|
444
|
+
if (block.atlasRows === 11) legacy.spriteVersionNumber = 2
|
|
445
|
+
else if (block.atlasRows !== DEFAULT_PET_ROW_COUNT) return undefined
|
|
446
|
+
}
|
|
447
|
+
if (manifest.sequences !== undefined) legacy.sequences = manifest.sequences
|
|
448
|
+
if (manifest.remarks !== undefined) legacy.remarks = manifest.remarks
|
|
449
|
+
return legacy
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Resolve a validated live2d manifest into a renderable entry (pet-center
|
|
454
|
+
* M3). The model3.json is read at scan time: its reference closure becomes
|
|
455
|
+
* the entry's servable set (the asset route's allow-list), and a model that
|
|
456
|
+
* is unreadable or declares unsafe references rejects the entry fail-closed
|
|
457
|
+
* with an error diagnostic. Closure files missing on disk warn but keep the
|
|
458
|
+
* entry listed — the client renderer's diagnostic card reports the broken
|
|
459
|
+
* render, matching the registry's never-throw philosophy (install-time
|
|
460
|
+
* strictness belongs to the CLI validator). The sprite fields carry contract
|
|
461
|
+
* defaults: the chrome sizes live2d pets off 'display.size', not the atlas.
|
|
462
|
+
*/
|
|
463
|
+
function resolveLive2dEntry(
|
|
464
|
+
manifest: PetManifestV2,
|
|
465
|
+
dir: string,
|
|
466
|
+
options: { assetPrefix?: string; warnings?: string[]; diagnostics?: PetRegistryDiagnostic[] },
|
|
467
|
+
): PetEntry | undefined {
|
|
468
|
+
const assetPrefix = options.assetPrefix ?? '/pet'
|
|
469
|
+
const record = (level: 'error' | 'warning', message: string): void => {
|
|
470
|
+
options.diagnostics?.push({ level, source: dir, message })
|
|
471
|
+
options.warnings?.push(message)
|
|
472
|
+
}
|
|
473
|
+
const block = manifest.live2d as PetManifestLive2d | undefined
|
|
474
|
+
if (block === undefined) {
|
|
475
|
+
record('error', 'pet ' + manifest.id + ': renderer live2d requires a live2d block')
|
|
476
|
+
return undefined
|
|
477
|
+
}
|
|
478
|
+
let model3: unknown
|
|
479
|
+
try {
|
|
480
|
+
model3 = JSON.parse(readFileSync(join(dir, block.model), 'utf8'))
|
|
481
|
+
} catch (error) {
|
|
482
|
+
record('error', 'pet ' + manifest.id + ': live2d model ' + block.model + ' is not readable: '
|
|
483
|
+
+ (error instanceof Error ? error.message : String(error)))
|
|
484
|
+
return undefined
|
|
485
|
+
}
|
|
486
|
+
const { references, errors } = collectModel3References(model3)
|
|
487
|
+
if (errors.length > 0) {
|
|
488
|
+
for (const message of errors) {
|
|
489
|
+
record('error', 'pet ' + manifest.id + ': live2d model ' + block.model + ': ' + message)
|
|
490
|
+
}
|
|
491
|
+
return undefined
|
|
492
|
+
}
|
|
493
|
+
for (const reference of references) {
|
|
494
|
+
if (!existsSync(join(dir, reference))) {
|
|
495
|
+
record('warning', 'pet ' + manifest.id + ': live2d closure file missing: ' + reference)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const tracks = buildTracks(DEFAULT_FRAME_COUNTS, DEFAULT_PET_COLUMNS, {}, message => record('warning', 'pet ' + manifest.id + ': ' + message))
|
|
499
|
+
if (tracks === undefined) return undefined
|
|
500
|
+
const remarks = normalizePetRemarks(manifest.remarks, message => record('warning', 'pet ' + manifest.id + ': ' + message))
|
|
501
|
+
const modelUrl = assetUrl(assetPrefix, manifest.id, block.model)
|
|
502
|
+
const live2d: PetLive2dDefinition = {
|
|
503
|
+
modelUrl,
|
|
504
|
+
modelPath: block.model,
|
|
505
|
+
...(block.scale === undefined ? {} : { scale: block.scale }),
|
|
506
|
+
...(block.translate === undefined ? {} : { translate: block.translate }),
|
|
507
|
+
motions: block.motions,
|
|
508
|
+
...(block.expressions === undefined ? {} : { expressions: block.expressions }),
|
|
509
|
+
...(block.hitAreas === undefined ? {} : { hitAreas: block.hitAreas }),
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
id: manifest.id,
|
|
513
|
+
displayName: manifest.displayName,
|
|
514
|
+
description: manifest.description ?? '',
|
|
515
|
+
renderer: 'live2d' as const,
|
|
516
|
+
live2d,
|
|
517
|
+
cell: { ...DEFAULT_PET_CELL },
|
|
518
|
+
columns: DEFAULT_PET_COLUMNS,
|
|
519
|
+
rows: [...DEFAULT_FRAME_COUNTS],
|
|
520
|
+
atlasRows: DEFAULT_PET_ROW_COUNT,
|
|
521
|
+
tracks,
|
|
522
|
+
atlasUrl: modelUrl,
|
|
523
|
+
manifestUrl: assetUrl(assetPrefix, manifest.id, 'pet.json'),
|
|
524
|
+
dir,
|
|
525
|
+
spritesheetPath: block.model,
|
|
526
|
+
servable: [block.model, ...references],
|
|
347
527
|
...(remarks === undefined ? {} : { remarks }),
|
|
348
528
|
}
|
|
349
529
|
}
|
|
350
530
|
|
|
351
531
|
/** Scan one directory of pet folders; entries come back in name order. */
|
|
352
|
-
function scanPetDir(dir: string, options: { assetPrefix?: string; warnings?: string[] }): PetEntry[] {
|
|
532
|
+
function scanPetDir(dir: string, options: { assetPrefix?: string; warnings?: string[]; diagnostics?: PetRegistryDiagnostic[] }): PetEntry[] {
|
|
353
533
|
if (!existsSync(dir)) return []
|
|
354
534
|
let names: string[] = []
|
|
355
535
|
try {
|
|
@@ -364,7 +544,26 @@ function scanPetDir(dir: string, options: { assetPrefix?: string; warnings?: str
|
|
|
364
544
|
if (!existsSync(manifestFile)) continue
|
|
365
545
|
const parsed = readPetJson(manifestFile, options.warnings)
|
|
366
546
|
if (parsed === undefined) continue
|
|
367
|
-
const
|
|
547
|
+
const entryDir = join(dir, name)
|
|
548
|
+
const verdict = parsePetManifest(parsed, entryDir)
|
|
549
|
+
for (const diagnostic of verdict.diagnostics) {
|
|
550
|
+
options.diagnostics?.push({ level: diagnostic.level, source: entryDir, message: diagnostic.message })
|
|
551
|
+
options.warnings?.push(diagnostic.message)
|
|
552
|
+
}
|
|
553
|
+
if (!verdict.ok) continue
|
|
554
|
+
if (verdict.manifest.renderer === 'live2d') {
|
|
555
|
+
const entry = resolveLive2dEntry(verdict.manifest, entryDir, options)
|
|
556
|
+
if (entry !== undefined) entries.push(entry)
|
|
557
|
+
continue
|
|
558
|
+
}
|
|
559
|
+
const legacy = flattenV2Sprite2d(verdict.manifest)
|
|
560
|
+
if (legacy === undefined) {
|
|
561
|
+
const note = 'pet ' + verdict.manifest.id + ': sprite2d.atlasRows only supports 9 or 11 under the v1 compat resolver'
|
|
562
|
+
options.diagnostics?.push({ level: 'error', source: entryDir, message: note })
|
|
563
|
+
options.warnings?.push(note)
|
|
564
|
+
continue
|
|
565
|
+
}
|
|
566
|
+
const entry = resolvePetManifest(legacy, entryDir, options)
|
|
368
567
|
if (entry !== undefined) entries.push(entry)
|
|
369
568
|
}
|
|
370
569
|
return entries
|
|
@@ -389,10 +588,11 @@ function readPetJson(file: string, warnings: string[] | undefined): unknown {
|
|
|
389
588
|
export function loadPetRegistry(options: PetRegistryOptions): PetRegistry {
|
|
390
589
|
const { packageRoot, assetPrefix = '/pet' } = options
|
|
391
590
|
const warnings: string[] = []
|
|
591
|
+
const diagnostics: PetRegistryDiagnostic[] = []
|
|
392
592
|
const byId = new Map<string, PetEntry>()
|
|
393
593
|
const builtinIds = new Set<string>()
|
|
394
594
|
|
|
395
|
-
for (const entry of scanPetDir(join(packageRoot, 'assets'), { assetPrefix, warnings })) {
|
|
595
|
+
for (const entry of scanPetDir(join(packageRoot, 'assets'), { assetPrefix, warnings, diagnostics })) {
|
|
396
596
|
if (byId.has(entry.id)) {
|
|
397
597
|
warnings.push('duplicate built-in pet id ' + entry.id + '; the first one wins')
|
|
398
598
|
continue
|
|
@@ -403,12 +603,21 @@ export function loadPetRegistry(options: PetRegistryOptions): PetRegistry {
|
|
|
403
603
|
|
|
404
604
|
const petsDir = options.petsDir ?? codexPetsDir()
|
|
405
605
|
if (petsDir !== '') {
|
|
406
|
-
for (const entry of scanPetDir(petsDir, { assetPrefix, warnings })) {
|
|
606
|
+
for (const entry of scanPetDir(petsDir, { assetPrefix, warnings, diagnostics })) {
|
|
407
607
|
if (byId.has(entry.id)) warnings.push('custom pet ' + entry.id + ' overrides the built-in one')
|
|
408
608
|
byId.set(entry.id, entry)
|
|
409
609
|
}
|
|
410
610
|
}
|
|
411
611
|
|
|
612
|
+
// The pet-center user directory ranks above the legacy hatch-pet source.
|
|
613
|
+
const dshPetsDir = options.dshPetsDir ?? join(dshHome(), 'pets')
|
|
614
|
+
if (dshPetsDir !== '') {
|
|
615
|
+
for (const entry of scanPetDir(dshPetsDir, { assetPrefix, warnings, diagnostics })) {
|
|
616
|
+
if (byId.has(entry.id)) warnings.push('user pet ' + entry.id + ' overrides an earlier registration')
|
|
617
|
+
byId.set(entry.id, entry)
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
412
621
|
for (const manifest of options.extra ?? []) {
|
|
413
622
|
const raw = manifest.spritesheetPath
|
|
414
623
|
const dir = raw === undefined || isAbsolute(raw)
|
|
@@ -431,6 +640,7 @@ export function loadPetRegistry(options: PetRegistryOptions): PetRegistry {
|
|
|
431
640
|
return {
|
|
432
641
|
entries,
|
|
433
642
|
warnings,
|
|
643
|
+
diagnostics,
|
|
434
644
|
byId: (id: string) => byId.get(id),
|
|
435
645
|
defaultEntry: () => entries.find(entry => builtinIds.has(entry.id)) ?? entries[0]!,
|
|
436
646
|
}
|
|
@@ -442,6 +652,8 @@ export function petEntryView(entry: PetEntry): PetDefinition {
|
|
|
442
652
|
id: entry.id,
|
|
443
653
|
displayName: entry.displayName,
|
|
444
654
|
description: entry.description,
|
|
655
|
+
renderer: entry.renderer,
|
|
656
|
+
...(entry.live2d === undefined ? {} : { live2d: entry.live2d }),
|
|
445
657
|
cell: entry.cell,
|
|
446
658
|
columns: entry.columns,
|
|
447
659
|
rows: entry.rows,
|
package/src/routes.ts
CHANGED
|
@@ -9,15 +9,16 @@
|
|
|
9
9
|
* @module @linxin666/dsh-pet/routes
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { existsSync } from 'node:fs'
|
|
12
|
+
import { existsSync, realpathSync, statSync } from 'node:fs'
|
|
13
13
|
import { readFile } from 'node:fs/promises'
|
|
14
|
-
import { join } from 'node:path'
|
|
14
|
+
import { join, sep } from 'node:path'
|
|
15
15
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
16
16
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
|
17
17
|
import type { PetService } from './service.ts'
|
|
18
18
|
import type { PetInteraction } from './affinity.ts'
|
|
19
|
-
import { petEntryView, type PetEntry, type PetRegistry } from './registry.ts'
|
|
19
|
+
import { petEntryView, petPackageRoot, type PetEntry, type PetRegistry } from './registry.ts'
|
|
20
20
|
import { isLoopbackRequest } from './loopback.ts'
|
|
21
|
+
import { dshHome } from './dsh-home.ts'
|
|
21
22
|
|
|
22
23
|
/** Browser-facing base path of the pet API. */
|
|
23
24
|
export const PET_API_PREFIX = '/api/pet'
|
|
@@ -29,6 +30,53 @@ const MANIFEST_FILE = 'pet.json'
|
|
|
29
30
|
const PREVIEW_DIR = 'previews'
|
|
30
31
|
const PREVIEW_PATTERN = /^[A-Za-z0-9._-]+$/
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Per-class size ceilings for served pet assets, in bytes (pet-center M2 P3,
|
|
35
|
+
* issue #623). Constants are tested directly; makePetRoutes accepts an
|
|
36
|
+
* override so tests can exercise the 413 path with tiny caps.
|
|
37
|
+
*/
|
|
38
|
+
export const PET_ASSET_CAPS = {
|
|
39
|
+
/** pet.json manifest. */
|
|
40
|
+
manifest: 64 * 1024,
|
|
41
|
+
/** Atlas, preview and Live2D texture imagery. */
|
|
42
|
+
image: 20 * 1024 * 1024,
|
|
43
|
+
/** Live2D model closure files (.moc3, motion/physics/expression JSON; M3). */
|
|
44
|
+
model: 32 * 1024 * 1024,
|
|
45
|
+
} as const
|
|
46
|
+
|
|
47
|
+
/** Size-cap profile the asset route enforces (test seam). */
|
|
48
|
+
export interface PetAssetCaps {
|
|
49
|
+
manifest: number
|
|
50
|
+
image: number
|
|
51
|
+
model: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Imagery extensions classify into the image cap; everything else served from a closure is model-class. */
|
|
55
|
+
const IMAGE_EXTENSIONS: ReadonlySet<string> = new Set(['.webp', '.png', '.gif', '.jpg', '.jpeg'])
|
|
56
|
+
|
|
57
|
+
/** Lowercased file extension ('' when none). */
|
|
58
|
+
function extensionOf(file: string): string {
|
|
59
|
+
const dot = file.lastIndexOf('.')
|
|
60
|
+
return dot < 0 ? '' : file.slice(dot).toLowerCase()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* realpath containment: resolve both sides and require the candidate to stay
|
|
65
|
+
* inside the base directory. A pet directory (or an atlas/preview inside it)
|
|
66
|
+
* that is a symlink escaping its root is rejected, never followed.
|
|
67
|
+
*/
|
|
68
|
+
export function containedRealpath(base: string, candidate: string): string | undefined {
|
|
69
|
+
try {
|
|
70
|
+
const realBase = realpathSync(base)
|
|
71
|
+
const realCandidate = realpathSync(candidate)
|
|
72
|
+
return realCandidate === realBase || realCandidate.startsWith(realBase + sep)
|
|
73
|
+
? realCandidate
|
|
74
|
+
: undefined
|
|
75
|
+
} catch {
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
32
80
|
const MIME_BY_EXT: Readonly<Record<string, string>> = {
|
|
33
81
|
'.webp': 'image/webp',
|
|
34
82
|
'.png': 'image/png',
|
|
@@ -145,10 +193,14 @@ function dirAliases(registry: PetRegistry): Map<string, PetEntry> {
|
|
|
145
193
|
/**
|
|
146
194
|
* The one asset handler behind the '/pet' prefix. Resolves the pet by id (or
|
|
147
195
|
* legacy directory alias), then serves exactly the files a manifest declares:
|
|
148
|
-
* pet.json, the
|
|
149
|
-
*
|
|
196
|
+
* pet.json, the entry's servable set (the sprite2d atlas, or the live2d
|
|
197
|
+
* model3.json plus its reference closure — pet-center M3), and optional
|
|
198
|
+
* 'previews/<name>' media. The servable match is an exact string comparison
|
|
199
|
+
* against scan-time normalized paths, so crafted '..' or '.' segments never
|
|
200
|
+
* match; containedRealpath stays as the second layer. Composed pets without
|
|
201
|
+
* a manifest file get a synthesized pet.json.
|
|
150
202
|
*/
|
|
151
|
-
function assetHandler(registry: PetRegistry): WebRoute['handler'] {
|
|
203
|
+
function assetHandler(registry: PetRegistry, caps: PetAssetCaps): WebRoute['handler'] {
|
|
152
204
|
const aliases = dirAliases(registry)
|
|
153
205
|
return (req: IncomingMessage, res: ServerResponse): void => {
|
|
154
206
|
if (!guard(req, res)) return
|
|
@@ -204,8 +256,8 @@ function assetHandler(registry: PetRegistry): WebRoute['handler'] {
|
|
|
204
256
|
const manifestFile = join(entry.dir, MANIFEST_FILE)
|
|
205
257
|
file = existsSync(manifestFile) ? manifestFile : undefined
|
|
206
258
|
if (file === undefined) synthesized = true
|
|
207
|
-
} else if (rest.length > 0 &&
|
|
208
|
-
file = join(entry.dir,
|
|
259
|
+
} else if (rest.length > 0 && entry.servable.includes(rel)) {
|
|
260
|
+
file = join(entry.dir, rel)
|
|
209
261
|
} else if (rest.length === 2 && rest[0] === PREVIEW_DIR && PREVIEW_PATTERN.test(rest[1]!)) {
|
|
210
262
|
const preview = join(entry.dir, PREVIEW_DIR, rest[1]!)
|
|
211
263
|
file = existsSync(preview) ? preview : undefined
|
|
@@ -229,7 +281,28 @@ function assetHandler(registry: PetRegistry): WebRoute['handler'] {
|
|
|
229
281
|
res.end()
|
|
230
282
|
return
|
|
231
283
|
}
|
|
232
|
-
|
|
284
|
+
// realpath containment: a symlink escaping the pet directory is refused.
|
|
285
|
+
const resolved = containedRealpath(entry.dir, file)
|
|
286
|
+
if (resolved === undefined) {
|
|
287
|
+
res.writeHead(403)
|
|
288
|
+
res.end()
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
// Enforce the size ceiling before the file is read into memory.
|
|
292
|
+
const cap = rest.length === 1 && rest[0] === MANIFEST_FILE
|
|
293
|
+
? caps.manifest
|
|
294
|
+
: IMAGE_EXTENSIONS.has(extensionOf(rel)) ? caps.image : caps.model
|
|
295
|
+
try {
|
|
296
|
+
if (statSync(resolved).size > cap) {
|
|
297
|
+
res.writeHead(413)
|
|
298
|
+
res.end()
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
res.writeHead(404)
|
|
303
|
+
res.end()
|
|
304
|
+
return
|
|
305
|
+
}
|
|
233
306
|
readFile(resolved).then((body) => {
|
|
234
307
|
res.writeHead(200, {
|
|
235
308
|
'content-type': mimeFor(resolved),
|
|
@@ -248,12 +321,119 @@ function assetHandler(registry: PetRegistry): WebRoute['handler'] {
|
|
|
248
321
|
}
|
|
249
322
|
}
|
|
250
323
|
|
|
251
|
-
/**
|
|
252
|
-
export
|
|
324
|
+
/** Browser-facing base path of the plugin runtime files (pet-center M3). */
|
|
325
|
+
export const PET_RUNTIME_PREFIX = PET_API_PREFIX + '/runtime'
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The runtime files the route may serve, by exact name (no slashes, no
|
|
329
|
+
* user-controlled path segments, so traversal is structurally impossible):
|
|
330
|
+
* the user-supplied Cubism Core from the pet runtime directory (the plugin
|
|
331
|
+
* never bundles or downloads it — issue #623 M1 §0) and the plugin-shipped
|
|
332
|
+
* MIT vendor bundle from the package lib directory.
|
|
333
|
+
*/
|
|
334
|
+
const RUNTIME_FILES: Readonly<Record<string, { root: 'runtimeDir' | 'vendorDir' }>> = {
|
|
335
|
+
'live2dcubismcore.min.js': { root: 'runtimeDir' },
|
|
336
|
+
'live2d-vendor.js': { root: 'vendorDir' },
|
|
337
|
+
'live2d-vendor.js.map': { root: 'vendorDir' },
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Size ceiling for one runtime file (the Cubism Core is ~200 KB today). */
|
|
341
|
+
export const PET_RUNTIME_CAP = 16 * 1024 * 1024
|
|
342
|
+
|
|
343
|
+
/** Runtime file roots (test seam; defaults resolve from the environment). */
|
|
344
|
+
export interface PetRuntimeRoots {
|
|
345
|
+
/** User-supplied runtime directory (defaults to '$DSH_HOME/pets/.runtime'). */
|
|
346
|
+
runtimeDir?: string
|
|
347
|
+
/** Plugin vendor bundle directory (defaults to the package 'lib'). */
|
|
348
|
+
vendorDir?: string
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The runtime handler behind '/api/pet/runtime/<name>'. A missing file
|
|
353
|
+
* answers 404 with a JSON marker the client renderer turns into install
|
|
354
|
+
* guidance (the Cubism Core is user-supplied, so its absence is a normal
|
|
355
|
+
* state, not an error).
|
|
356
|
+
*/
|
|
357
|
+
function runtimeHandler(roots: { runtimeDir: string; vendorDir: string }): WebRoute['handler'] {
|
|
358
|
+
return (req: IncomingMessage, res: ServerResponse): void => {
|
|
359
|
+
if (!guard(req, res)) return
|
|
360
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
361
|
+
res.writeHead(405)
|
|
362
|
+
res.end()
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
let pathname: string
|
|
366
|
+
try {
|
|
367
|
+
pathname = new URL(req.url ?? '/', 'http://pet.local').pathname
|
|
368
|
+
} catch {
|
|
369
|
+
res.writeHead(400)
|
|
370
|
+
res.end()
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
const rest = pathname.slice(PET_RUNTIME_PREFIX.length).replace(/^\/+/, '')
|
|
374
|
+
let name: string
|
|
375
|
+
try {
|
|
376
|
+
name = decodeURIComponent(rest)
|
|
377
|
+
} catch {
|
|
378
|
+
res.writeHead(400)
|
|
379
|
+
res.end()
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
const spec = RUNTIME_FILES[name]
|
|
383
|
+
// Exact-name allow-list: anything with a path separator never matches.
|
|
384
|
+
if (spec === undefined) {
|
|
385
|
+
res.writeHead(404)
|
|
386
|
+
res.end()
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
const base = spec.root === 'runtimeDir' ? roots.runtimeDir : roots.vendorDir
|
|
390
|
+
const file = join(base, name)
|
|
391
|
+
if (!existsSync(file)) {
|
|
392
|
+
json(res, 404, { ok: false, error: 'runtime-file-missing', file: name })
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
const resolved = containedRealpath(base, file)
|
|
396
|
+
if (resolved === undefined) {
|
|
397
|
+
res.writeHead(403)
|
|
398
|
+
res.end()
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
if (statSync(resolved).size > PET_RUNTIME_CAP) {
|
|
403
|
+
res.writeHead(413)
|
|
404
|
+
res.end()
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
} catch {
|
|
408
|
+
res.writeHead(404)
|
|
409
|
+
res.end()
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
readFile(resolved).then((body) => {
|
|
413
|
+
res.writeHead(200, {
|
|
414
|
+
'content-type': name.endsWith('.map') ? 'application/json' : 'application/javascript; charset=utf-8',
|
|
415
|
+
'content-length': String(body.byteLength),
|
|
416
|
+
'cache-control': 'no-cache',
|
|
417
|
+
})
|
|
418
|
+
if (req.method === 'HEAD') {
|
|
419
|
+
res.end()
|
|
420
|
+
return
|
|
421
|
+
}
|
|
422
|
+
res.end(body)
|
|
423
|
+
}, () => {
|
|
424
|
+
res.writeHead(404)
|
|
425
|
+
res.end()
|
|
426
|
+
})
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Build the full route family (API + assets + runtime) for one service. */
|
|
431
|
+
export function makePetRoutes(deps: { service: PetService; assetCaps?: PetAssetCaps } & PetRuntimeRoots): WebRoute[] {
|
|
253
432
|
const { service } = deps
|
|
254
433
|
const apiRoutes: WebRoute[] = [
|
|
255
434
|
getRoute(PET_API_PREFIX + '/state', () => service.state()),
|
|
256
435
|
getRoute(PET_API_PREFIX + '/pets', () => service.pets()),
|
|
436
|
+
getRoute(PET_API_PREFIX + '/diagnostics', () => service.diagnostics()),
|
|
257
437
|
postRoute(PET_API_PREFIX + '/interact', (body) => {
|
|
258
438
|
const kind = body.kind as PetInteraction | undefined
|
|
259
439
|
if (kind !== 'pet' && kind !== 'feed') return Promise.reject(new Error('invalid-kind'))
|
|
@@ -285,10 +465,19 @@ export function makePetRoutes(deps: { service: PetService }): WebRoute[] {
|
|
|
285
465
|
const assetRoute: WebRoute = {
|
|
286
466
|
kind: 'prefix',
|
|
287
467
|
path: PET_ASSET_PREFIX,
|
|
288
|
-
handler: assetHandler(service.registrySnapshot()),
|
|
468
|
+
handler: assetHandler(service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const runtimeRoute: WebRoute = {
|
|
472
|
+
kind: 'prefix',
|
|
473
|
+
path: PET_RUNTIME_PREFIX,
|
|
474
|
+
handler: runtimeHandler({
|
|
475
|
+
runtimeDir: deps.runtimeDir ?? join(dshHome(), 'pets', '.runtime'),
|
|
476
|
+
vendorDir: deps.vendorDir ?? join(petPackageRoot(import.meta.url), 'lib'),
|
|
477
|
+
}),
|
|
289
478
|
}
|
|
290
479
|
|
|
291
|
-
return [...apiRoutes, assetRoute]
|
|
480
|
+
return [...apiRoutes, assetRoute, runtimeRoute]
|
|
292
481
|
}
|
|
293
482
|
|
|
294
483
|
// Re-exported for the package surface (the registry owns the definition now).
|