fix: quiet missing avatar preferences on first run

Treat a missing runtime/avatar-preferences.json as the default first-run state instead of surfacing a warning, while keeping malformed files visible as normalized fallback issues.

Refs #52.
This commit is contained in:
OpenClaw Local
2026-04-11 17:11:11 +02:00
parent 8172e6fb0e
commit 0895a0d794
2 changed files with 87 additions and 0 deletions
+16
View File
@@ -48,6 +48,13 @@ export async function loadAvatarPreferences(): Promise<AvatarPreferencesLoadResu
parsed = JSON.parse(raw) as unknown;
} catch (error) {
const fallback = defaultAvatarPreferences();
if (isNotFoundError(error)) {
return {
path: AVATAR_PREFERENCES_PATH,
preferences: fallback,
issues,
};
}
const reason = error instanceof Error ? error.message : "unable to read preference file";
issues = [`avatar preferences fallback applied: ${reason}`];
// 只读模式:不自动创建文件,只返回默认配置
@@ -228,6 +235,15 @@ function asObject(input: unknown): Record<string, unknown> | undefined {
return input as Record<string, unknown>;
}
function isNotFoundError(error: unknown): boolean {
return Boolean(
error &&
typeof error === "object" &&
"code" in error &&
(error as { code?: unknown }).code === "ENOENT",
);
}
async function writeAvatarPreferences(preferences: AvatarPreferences): Promise<void> {
await mkdir(join(process.cwd(), "runtime"), { recursive: true });
await writeFile(AVATAR_PREFERENCES_PATH, `${JSON.stringify(preferences, null, 2)}\n`, "utf8");
+71
View File
@@ -0,0 +1,71 @@
import assert from "node:assert/strict";
import test from "node:test";
import { readFile, rm, writeFile } from "node:fs/promises";
import {
AVATAR_PREFERENCES_PATH,
defaultAvatarPreferences,
loadAvatarPreferences,
saveAvatarPreferences,
} from "../src/runtime/avatar-preferences";
test("missing avatar preferences use defaults without first-run warnings", async () => {
const original = await readMaybe(AVATAR_PREFERENCES_PATH);
try {
await rm(AVATAR_PREFERENCES_PATH, { force: true });
const loaded = await loadAvatarPreferences();
assert.deepEqual(loaded.preferences.agents, {});
assert.equal(loaded.preferences.version, 1);
assert.deepEqual(loaded.issues, []);
} finally {
if (original === undefined) {
await rm(AVATAR_PREFERENCES_PATH, { force: true });
} else {
await writeFile(AVATAR_PREFERENCES_PATH, original, "utf8");
}
}
});
test("avatar preferences still persist after save and reload", async () => {
const original = await readMaybe(AVATAR_PREFERENCES_PATH);
try {
await saveAvatarPreferences({
...defaultAvatarPreferences(),
agents: {
main: {
mode: "pixel",
animal: "panda",
updatedAt: new Date().toISOString(),
},
},
});
const loaded = await loadAvatarPreferences();
assert.equal(loaded.preferences.agents.main?.mode, "pixel");
assert.equal(loaded.preferences.agents.main?.animal, "panda");
} finally {
if (original === undefined) {
await rm(AVATAR_PREFERENCES_PATH, { force: true });
} else {
await writeFile(AVATAR_PREFERENCES_PATH, original, "utf8");
}
}
});
async function readMaybe(path: string): Promise<string | undefined> {
try {
return await readFile(path, "utf8");
} catch (error) {
if (
error &&
typeof error === "object" &&
"code" in error &&
(error as { code?: unknown }).code === "ENOENT"
) {
return undefined;
}
throw error;
}
}