fix(stage-ui-live2d,stage-tamagotchi): use temp file to prevent config loss and handle more Live2D model load failures (#1233)

This commit is contained in:
Pratyush Sharma
2026-03-10 17:35:58 +08:00
committed by GitHub
parent 396dd9615c
commit 2b5cebed1e
4 changed files with 48 additions and 3 deletions
@@ -1,7 +1,7 @@
import type { BaseIssue, BaseSchema, InferIssue, InferOutput } from 'valibot'
import { existsSync, readFileSync } from 'node:fs'
import { writeFile } from 'node:fs/promises'
import { copyFile, rename, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { safeDestr } from 'destr'
@@ -74,7 +74,10 @@ export function createConfig<TSchema extends PersistedSchema>(
const save = throttle(async () => {
try {
await writeFile(configPath(), JSON.stringify(persistenceMap.get(key)))
const path = configPath()
const tmpPath = `${path}.tmp`
await writeFile(tmpPath, JSON.stringify(persistenceMap.get(key)))
await rename(tmpPath, path)
}
catch (error) {
console.error('Failed to save config', error)
@@ -83,7 +86,11 @@ export function createConfig<TSchema extends PersistedSchema>(
const writeHealingConfig = async (value: InferOutput<TSchema>) => {
try {
await writeFile(configPath(), JSON.stringify(value))
const path = configPath()
if (existsSync(path)) {
await copyFile(path, `${path}.bak`).catch(err => console.warn('Failed to create backup for config:', path, err))
}
await writeFile(path, JSON.stringify(value))
return true
}
catch (error) {
@@ -61,6 +61,7 @@ const props = withDefaults(defineProps<{
const emits = defineEmits<{
(e: 'modelLoaded'): void
(e: 'error', error: Error): void
}>()
const componentState = defineModel<'pending' | 'loading' | 'mounted'>('state', { default: 'pending' })
@@ -369,6 +370,10 @@ async function loadModel() {
emits('modelLoaded')
}
catch (error) {
console.error('[Live2D] Failed to load model:', error)
emits('error', error instanceof Error ? error : new Error(String(error)))
}
finally {
modelLoading.value = false
componentState.value = 'mounted'
@@ -12,6 +12,18 @@ declare global {
}
export class OPFSCache {
static async clearAll(): Promise<void> {
try {
const root = await navigator.storage.getDirectory()
for await (const entry of root.values()) {
await root.removeEntry(entry.name, { recursive: true })
}
}
catch (e) {
console.error('[OPFS] Failed to clear cache:', e)
}
}
static async readDirectoryRecursive(dir: FileSystemDirectoryHandle, pathPrefix: string): Promise<File[]> {
const files: File[] = []
for await (const entry of dir.values()) {
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { defaultModelParameters, useLive2d } from '@proj-airi/stage-ui-live2d'
import { OPFSCache } from '@proj-airi/stage-ui-live2d/utils/opfs-loader'
import { Button, Checkbox, FieldRange, SelectTab } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
@@ -81,6 +82,18 @@ function resetToDefaultParameters() {
modelParameters.value = { ...defaultModelParameters }
}
const clearingCache = ref(false)
async function clearModelCache() {
clearingCache.value = true
try {
await OPFSCache.clearAll()
}
finally {
clearingCache.value = false
}
}
// Runtime motion selection handlers
function handleMotionSelect(motion: any) {
selectedRuntimeMotion.value = motion.displayPath // Store full path
@@ -375,6 +388,14 @@ onUnmounted(() => {
Reset To Default Parameters
</button>
<button
mt-2 w-full border rounded bg-neutral-100 px-4 py-2 text-sm text-neutral-700 font-medium transition-colors dark:border-neutral-700 dark:bg-neutral-800 hover:bg-neutral-200 dark:text-neutral-300 dark:hover:bg-neutral-700
:disabled="clearingCache"
@click="clearModelCache"
>
{{ clearingCache ? 'Clearing...' : 'Clear Model Cache' }}
</button>
<!-- Head Rotation -->
<div mb-2 mt-4 text-xs text-neutral-500 font-semibold dark:text-neutral-400>
Head Rotation