flawed-avatar 0.2.1
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/assets/animations/idle/Breathing Idle.fbx +0 -0
- package/assets/animations/idle/look away gesture.fbx +0 -0
- package/assets/animations/idle/weight shift.fbx +0 -0
- package/assets/animations/speaking/Agreeing.fbx +0 -0
- package/assets/animations/speaking/Talking (1).fbx +0 -0
- package/assets/animations/speaking/Talking (2).fbx +0 -0
- package/assets/animations/speaking/Talking (3).fbx +0 -0
- package/assets/animations/speaking/Talking.fbx +0 -0
- package/assets/animations/speaking/head nod yes.fbx +0 -0
- package/assets/animations/thinking/Thinking.fbx +0 -0
- package/assets/animations/thinking/thoughtful head shake.fbx +0 -0
- package/assets/animations/working/acknowledging.fbx +0 -0
- package/assets/animations/working/lengthy head nod.fbx +0 -0
- package/assets/icon.png +0 -0
- package/assets/models/CaptainLobster.vrm +0 -0
- package/assets/models/default-avatar.vrm +0 -0
- package/dist/chat-preload.cjs +87 -0
- package/dist/chat-renderer-bundle/chat-index.html +16 -0
- package/dist/chat-renderer-bundle/chat-renderer.js +355 -0
- package/dist/chat-renderer-bundle/styles/base.css +106 -0
- package/dist/chat-renderer-bundle/styles/chat.css +516 -0
- package/dist/chat-renderer-bundle/styles/components/button.css +221 -0
- package/dist/chat-renderer-bundle/styles/components/indicator.css +216 -0
- package/dist/chat-renderer-bundle/styles/components/input.css +139 -0
- package/dist/chat-renderer-bundle/styles/components/toast.css +204 -0
- package/dist/chat-renderer-bundle/styles/controls.css +279 -0
- package/dist/chat-renderer-bundle/styles/settings.css +310 -0
- package/dist/chat-renderer-bundle/styles/tokens.css +220 -0
- package/dist/chat-renderer-bundle/styles/utilities.css +349 -0
- package/dist/main/main/display-utils.d.ts +12 -0
- package/dist/main/main/display-utils.js +29 -0
- package/dist/main/main/gateway-client.d.ts +13 -0
- package/dist/main/main/gateway-client.js +265 -0
- package/dist/main/main/main.d.ts +1 -0
- package/dist/main/main/main.js +157 -0
- package/dist/main/main/persistence/chat-store.d.ts +8 -0
- package/dist/main/main/persistence/chat-store.js +110 -0
- package/dist/main/main/persistence/file-store.d.ts +17 -0
- package/dist/main/main/persistence/file-store.js +183 -0
- package/dist/main/main/persistence/index.d.ts +8 -0
- package/dist/main/main/persistence/index.js +8 -0
- package/dist/main/main/persistence/migrations.d.ts +23 -0
- package/dist/main/main/persistence/migrations.js +191 -0
- package/dist/main/main/persistence/settings-store.d.ts +32 -0
- package/dist/main/main/persistence/settings-store.js +174 -0
- package/dist/main/main/persistence/types.d.ts +72 -0
- package/dist/main/main/persistence/types.js +69 -0
- package/dist/main/main/settings-broadcast.d.ts +3 -0
- package/dist/main/main/settings-broadcast.js +9 -0
- package/dist/main/main/stdin-listener.d.ts +15 -0
- package/dist/main/main/stdin-listener.js +27 -0
- package/dist/main/main/tray.d.ts +3 -0
- package/dist/main/main/tray.js +59 -0
- package/dist/main/main/window-manager.d.ts +23 -0
- package/dist/main/main/window-manager.js +232 -0
- package/dist/main/main/window.d.ts +3 -0
- package/dist/main/main/window.js +528 -0
- package/dist/main/shared/config.d.ts +91 -0
- package/dist/main/shared/config.js +111 -0
- package/dist/main/shared/ipc-channels.d.ts +54 -0
- package/dist/main/shared/ipc-channels.js +68 -0
- package/dist/main/shared/types.d.ts +6 -0
- package/dist/main/shared/types.js +1 -0
- package/dist/preload.cjs +256 -0
- package/dist/renderer-bundle/index.html +63 -0
- package/dist/renderer-bundle/renderer.js +100734 -0
- package/dist/renderer-bundle/styles/base.css +106 -0
- package/dist/renderer-bundle/styles/chat.css +516 -0
- package/dist/renderer-bundle/styles/components/button.css +221 -0
- package/dist/renderer-bundle/styles/components/indicator.css +216 -0
- package/dist/renderer-bundle/styles/components/input.css +139 -0
- package/dist/renderer-bundle/styles/components/toast.css +204 -0
- package/dist/renderer-bundle/styles/controls.css +279 -0
- package/dist/renderer-bundle/styles/settings.css +310 -0
- package/dist/renderer-bundle/styles/tokens.css +220 -0
- package/dist/renderer-bundle/styles/utilities.css +349 -0
- package/index.ts +32 -0
- package/openclaw.plugin.json +22 -0
- package/package.json +45 -0
- package/src/electron-launcher.ts +63 -0
- package/src/main/chat-preload.cjs +87 -0
- package/src/main/display-utils.ts +39 -0
- package/src/main/gateway-client.ts +312 -0
- package/src/main/main.ts +169 -0
- package/src/main/persistence/chat-store.ts +143 -0
- package/src/main/persistence/file-store.ts +221 -0
- package/src/main/persistence/index.ts +69 -0
- package/src/main/persistence/migrations.ts +232 -0
- package/src/main/persistence/settings-store.ts +219 -0
- package/src/main/persistence/types.ts +107 -0
- package/src/main/preload.cjs +256 -0
- package/src/main/settings-broadcast.ts +13 -0
- package/src/main/settings-preload.cjs +153 -0
- package/src/main/stdin-listener.ts +34 -0
- package/src/main/tray.ts +65 -0
- package/src/main/window-manager.ts +298 -0
- package/src/main/window.ts +614 -0
- package/src/renderer/audio/audio-player.ts +161 -0
- package/src/renderer/audio/frequency-analyzer.ts +104 -0
- package/src/renderer/audio/index.ts +36 -0
- package/src/renderer/audio/kokoro-model-loader.ts +128 -0
- package/src/renderer/audio/kokoro-tts-service.ts +370 -0
- package/src/renderer/audio/lip-sync-profile.json +1 -0
- package/src/renderer/audio/phoneme-mapper.ts +120 -0
- package/src/renderer/audio/tts-controller.ts +344 -0
- package/src/renderer/audio/tts-service-factory.ts +75 -0
- package/src/renderer/audio/tts-service.ts +16 -0
- package/src/renderer/audio/types.ts +120 -0
- package/src/renderer/audio/web-speech-tts.ts +177 -0
- package/src/renderer/audio/wlipsync-analyzer.ts +145 -0
- package/src/renderer/avatar/animation-loader.ts +114 -0
- package/src/renderer/avatar/animator.ts +322 -0
- package/src/renderer/avatar/expressions.ts +165 -0
- package/src/renderer/avatar/eye-gaze.ts +255 -0
- package/src/renderer/avatar/eye-saccades.ts +133 -0
- package/src/renderer/avatar/hover-awareness.ts +125 -0
- package/src/renderer/avatar/ibl-enhancer.ts +163 -0
- package/src/renderer/avatar/lip-sync.ts +258 -0
- package/src/renderer/avatar/mixamo-retarget.ts +169 -0
- package/src/renderer/avatar/pixel-transparency.ts +65 -0
- package/src/renderer/avatar/scene.ts +70 -0
- package/src/renderer/avatar/spring-bones.ts +27 -0
- package/src/renderer/avatar/state-machine.ts +117 -0
- package/src/renderer/avatar/vrm-loader.ts +71 -0
- package/src/renderer/chat-window/chat-index.html +16 -0
- package/src/renderer/chat-window/chat-renderer.ts +28 -0
- package/src/renderer/index.html +63 -0
- package/src/renderer/renderer.ts +329 -0
- package/src/renderer/settings-window/settings-controls.ts +223 -0
- package/src/renderer/settings-window/settings-index.html +16 -0
- package/src/renderer/settings-window/settings-panel.ts +346 -0
- package/src/renderer/settings-window/settings-renderer.ts +5 -0
- package/src/renderer/styles/base.css +106 -0
- package/src/renderer/styles/chat.css +516 -0
- package/src/renderer/styles/components/button.css +221 -0
- package/src/renderer/styles/components/indicator.css +216 -0
- package/src/renderer/styles/components/input.css +139 -0
- package/src/renderer/styles/components/toast.css +204 -0
- package/src/renderer/styles/controls.css +279 -0
- package/src/renderer/styles/settings.css +310 -0
- package/src/renderer/styles/tokens.css +220 -0
- package/src/renderer/styles/utilities.css +349 -0
- package/src/renderer/types/avatar-bridge.d.ts +86 -0
- package/src/renderer/types/chat-bridge.d.ts +37 -0
- package/src/renderer/types/settings-bridge.d.ts +54 -0
- package/src/renderer/ui/chat-bubble.ts +435 -0
- package/src/renderer/ui/icons.ts +47 -0
- package/src/renderer/ui/typing-indicator.ts +41 -0
- package/src/service.ts +163 -0
- package/src/shared/config.ts +135 -0
- package/src/shared/ipc-channels.ts +81 -0
- package/src/shared/types.ts +7 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import { SETTINGS_DEBOUNCE_MS, LOCK_TIMEOUT_MS, LOCK_STALE_MS, } from "../../shared/config.js";
|
|
5
|
+
const openclawDir = path.join(os.homedir(), ".openclaw");
|
|
6
|
+
function ensureDir() {
|
|
7
|
+
try {
|
|
8
|
+
fs.mkdirSync(openclawDir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// Directory likely already exists
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function getLockPath(filePath) {
|
|
15
|
+
return `${filePath}.lock`;
|
|
16
|
+
}
|
|
17
|
+
function acquireLock(filePath) {
|
|
18
|
+
const lockPath = getLockPath(filePath);
|
|
19
|
+
try {
|
|
20
|
+
// Check for stale lock
|
|
21
|
+
try {
|
|
22
|
+
const stat = fs.statSync(lockPath);
|
|
23
|
+
const age = Date.now() - stat.mtimeMs;
|
|
24
|
+
if (age > LOCK_STALE_MS) {
|
|
25
|
+
// Lock is stale, remove it
|
|
26
|
+
fs.unlinkSync(lockPath);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Lock doesn't exist, good
|
|
31
|
+
}
|
|
32
|
+
// Try to create lock file exclusively
|
|
33
|
+
fs.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function releaseLock(filePath) {
|
|
41
|
+
const lockPath = getLockPath(filePath);
|
|
42
|
+
try {
|
|
43
|
+
fs.unlinkSync(lockPath);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Lock already released or never acquired
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function waitForLock(filePath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
50
|
+
const startTime = Date.now();
|
|
51
|
+
const interval = 50;
|
|
52
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
53
|
+
if (acquireLock(filePath)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
export function createFileStore(options) {
|
|
61
|
+
const { filename, schema, defaultValue, debounceMs = SETTINGS_DEBOUNCE_MS } = options;
|
|
62
|
+
const filePath = path.join(openclawDir, filename);
|
|
63
|
+
let cache = null;
|
|
64
|
+
let pendingData = null;
|
|
65
|
+
let saveTimeout = null;
|
|
66
|
+
let isFlushing = false;
|
|
67
|
+
function load() {
|
|
68
|
+
try {
|
|
69
|
+
const data = fs.readFileSync(filePath, "utf-8");
|
|
70
|
+
const parsed = JSON.parse(data);
|
|
71
|
+
const result = schema.safeParse(parsed);
|
|
72
|
+
if (result.success) {
|
|
73
|
+
cache = result.data;
|
|
74
|
+
return { ok: true, data: result.data };
|
|
75
|
+
}
|
|
76
|
+
// Schema validation failed
|
|
77
|
+
console.warn(`[file-store] Schema validation failed for ${filename}:`, result.error.message);
|
|
78
|
+
const fallback = defaultValue();
|
|
79
|
+
cache = fallback;
|
|
80
|
+
return { ok: false, error: result.error.message, fallback };
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
// File not found or parse error - normal for first run
|
|
84
|
+
const fallback = defaultValue();
|
|
85
|
+
cache = fallback;
|
|
86
|
+
if (err.code !== "ENOENT") {
|
|
87
|
+
console.warn(`[file-store] Error reading ${filename}:`, err);
|
|
88
|
+
return { ok: false, error: String(err), fallback };
|
|
89
|
+
}
|
|
90
|
+
return { ok: true, data: fallback };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function scheduleWrite(data) {
|
|
94
|
+
pendingData = data;
|
|
95
|
+
cache = data;
|
|
96
|
+
if (saveTimeout) {
|
|
97
|
+
clearTimeout(saveTimeout);
|
|
98
|
+
}
|
|
99
|
+
saveTimeout = setTimeout(() => {
|
|
100
|
+
void performWrite();
|
|
101
|
+
}, debounceMs);
|
|
102
|
+
}
|
|
103
|
+
async function performWrite() {
|
|
104
|
+
if (pendingData === null || isFlushing)
|
|
105
|
+
return;
|
|
106
|
+
isFlushing = true;
|
|
107
|
+
const dataToWrite = pendingData;
|
|
108
|
+
pendingData = null;
|
|
109
|
+
saveTimeout = null;
|
|
110
|
+
const lockAcquired = await waitForLock(filePath);
|
|
111
|
+
if (!lockAcquired) {
|
|
112
|
+
console.warn(`[file-store] Failed to acquire lock for ${filename}, will retry on next save`);
|
|
113
|
+
pendingData = dataToWrite; // Restore pending data for retry
|
|
114
|
+
isFlushing = false;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
ensureDir();
|
|
119
|
+
// On Windows, atomic rename doesn't work well across drives,
|
|
120
|
+
// so we write directly
|
|
121
|
+
const content = JSON.stringify(dataToWrite, null, "\t");
|
|
122
|
+
if (process.platform === "win32") {
|
|
123
|
+
fs.writeFileSync(filePath, content);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// Atomic write: temp file + rename
|
|
127
|
+
const tempPath = `${filePath}.tmp.${process.pid}`;
|
|
128
|
+
fs.writeFileSync(tempPath, content);
|
|
129
|
+
fs.renameSync(tempPath, filePath);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
console.warn(`[file-store] Error writing ${filename}:`, err);
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
releaseLock(filePath);
|
|
137
|
+
isFlushing = false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function save(data) {
|
|
141
|
+
scheduleWrite(data);
|
|
142
|
+
}
|
|
143
|
+
async function flush() {
|
|
144
|
+
if (saveTimeout) {
|
|
145
|
+
clearTimeout(saveTimeout);
|
|
146
|
+
saveTimeout = null;
|
|
147
|
+
}
|
|
148
|
+
if (pendingData !== null) {
|
|
149
|
+
await performWrite();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function getCache() {
|
|
153
|
+
return cache;
|
|
154
|
+
}
|
|
155
|
+
function cleanup() {
|
|
156
|
+
if (saveTimeout) {
|
|
157
|
+
clearTimeout(saveTimeout);
|
|
158
|
+
saveTimeout = null;
|
|
159
|
+
}
|
|
160
|
+
// Synchronous flush on cleanup
|
|
161
|
+
if (pendingData !== null) {
|
|
162
|
+
try {
|
|
163
|
+
ensureDir();
|
|
164
|
+
const content = JSON.stringify(pendingData, null, "\t");
|
|
165
|
+
fs.writeFileSync(filePath, content);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Ignore errors during cleanup
|
|
169
|
+
}
|
|
170
|
+
pendingData = null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
load,
|
|
175
|
+
save,
|
|
176
|
+
flush,
|
|
177
|
+
getCache,
|
|
178
|
+
cleanup,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export function getOpenclawDir() {
|
|
182
|
+
return openclawDir;
|
|
183
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { Settings, ChatMessage, ChatHistory, LoadResult, } from "./types.js";
|
|
2
|
+
export type { LightingCustom } from "./types.js";
|
|
3
|
+
export { SETTINGS_SCHEMA_VERSION, CHAT_SCHEMA_VERSION, SettingsSchema, LightingCustomSchema, ChatMessageSchema, ChatHistorySchema, createDefaultSettings, createDefaultChatHistory, } from "./types.js";
|
|
4
|
+
export type { FileStore, StoreOptions } from "./file-store.js";
|
|
5
|
+
export { createFileStore, getOpenclawDir } from "./file-store.js";
|
|
6
|
+
export { loadSettings, saveSettings, savePosition, saveZoom, saveOpacity, saveIdleTimeout, saveTtsEnabled, saveTtsEngine, saveTtsVoice, saveVrmModelPath, getPosition, getZoom, getOpacity, getIdleTimeout, getTtsEnabled, getTtsEngine, getTtsVoice, getVrmModelPath, saveScale, getScale, saveLightingProfile, getLightingProfile, saveLightingCustom, getLightingCustom, flushSettings, cleanupSettings, getSettingsStore, } from "./settings-store.js";
|
|
7
|
+
export { loadChatHistory, appendMessage, clearChatHistory, getRecentMessages, getChatHistory, flushChat, cleanupChat, } from "./chat-store.js";
|
|
8
|
+
export { migrateFileNames, migrateLegacyFiles, migrateV1ToV2, migrateV2ToV3 } from "./migrations.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { SETTINGS_SCHEMA_VERSION, CHAT_SCHEMA_VERSION, SettingsSchema, LightingCustomSchema, ChatMessageSchema, ChatHistorySchema, createDefaultSettings, createDefaultChatHistory, } from "./types.js";
|
|
2
|
+
export { createFileStore, getOpenclawDir } from "./file-store.js";
|
|
3
|
+
// Settings store
|
|
4
|
+
export { loadSettings, saveSettings, savePosition, saveZoom, saveOpacity, saveIdleTimeout, saveTtsEnabled, saveTtsEngine, saveTtsVoice, saveVrmModelPath, getPosition, getZoom, getOpacity, getIdleTimeout, getTtsEnabled, getTtsEngine, getTtsVoice, getVrmModelPath, saveScale, getScale, saveLightingProfile, getLightingProfile, saveLightingCustom, getLightingCustom, flushSettings, cleanupSettings, getSettingsStore, } from "./settings-store.js";
|
|
5
|
+
// Chat store
|
|
6
|
+
export { loadChatHistory, appendMessage, clearChatHistory, getRecentMessages, getChatHistory, flushChat, cleanupChat, } from "./chat-store.js";
|
|
7
|
+
// Migrations
|
|
8
|
+
export { migrateFileNames, migrateLegacyFiles, migrateV1ToV2, migrateV2ToV3 } from "./migrations.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Settings } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Renames old avatar-overlay-*.json files to flawed-avatar-*.json
|
|
4
|
+
* so existing users' settings survive the package rename.
|
|
5
|
+
*/
|
|
6
|
+
export declare function migrateFileNames(): void;
|
|
7
|
+
/**
|
|
8
|
+
* Migrates legacy separate files (position.json, camera.json) to unified settings.
|
|
9
|
+
* Returns the migrated settings if migration occurred, null otherwise.
|
|
10
|
+
* Safe to call multiple times - only migrates if legacy files exist.
|
|
11
|
+
*/
|
|
12
|
+
export declare function migrateLegacyFiles(): Settings | null;
|
|
13
|
+
/**
|
|
14
|
+
* Migrates settings from schema v1 to v2.
|
|
15
|
+
* Converts position from `{x,y}` flat object to `Record<displayHash, {x,y}>`.
|
|
16
|
+
* Reads raw JSON to avoid schema validation rejecting the old format.
|
|
17
|
+
*/
|
|
18
|
+
export declare function migrateV1ToV2(): void;
|
|
19
|
+
/**
|
|
20
|
+
* Migrates settings from schema v2 to v3.
|
|
21
|
+
* Adds `scale` and `lightingProfile` fields with defaults.
|
|
22
|
+
*/
|
|
23
|
+
export declare function migrateV2ToV3(): void;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { WINDOW_POSITION_FILE, CAMERA_ZOOM_FILE, CAMERA_ZOOM_MIN, CAMERA_ZOOM_MAX, SCALE_DEFAULT, LIGHTING_PROFILE_DEFAULT, } from "../../shared/config.js";
|
|
4
|
+
import { getOpenclawDir } from "./file-store.js";
|
|
5
|
+
import { createDefaultSettings, SETTINGS_SCHEMA_VERSION, } from "./types.js";
|
|
6
|
+
import { loadSettings, getSettingsStore } from "./settings-store.js";
|
|
7
|
+
import { computeDisplayHash } from "../display-utils.js";
|
|
8
|
+
/**
|
|
9
|
+
* Renames old avatar-overlay-*.json files to flawed-avatar-*.json
|
|
10
|
+
* so existing users' settings survive the package rename.
|
|
11
|
+
*/
|
|
12
|
+
export function migrateFileNames() {
|
|
13
|
+
const dir = getOpenclawDir();
|
|
14
|
+
const renames = [
|
|
15
|
+
["avatar-overlay-settings.json", "flawed-avatar-settings.json"],
|
|
16
|
+
["avatar-overlay-position.json", "flawed-avatar-position.json"],
|
|
17
|
+
["avatar-overlay-camera.json", "flawed-avatar-camera.json"],
|
|
18
|
+
["avatar-overlay-chat.json", "flawed-avatar-chat.json"],
|
|
19
|
+
];
|
|
20
|
+
for (const [oldName, newName] of renames) {
|
|
21
|
+
const oldPath = path.join(dir, oldName);
|
|
22
|
+
const newPath = path.join(dir, newName);
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(oldPath) && !fs.existsSync(newPath)) {
|
|
25
|
+
fs.renameSync(oldPath, newPath);
|
|
26
|
+
console.log(`[migrations] Renamed ${oldName} → ${newName}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Best-effort; next run will retry
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function readLegacyPosition() {
|
|
35
|
+
const filePath = path.join(getOpenclawDir(), WINDOW_POSITION_FILE);
|
|
36
|
+
try {
|
|
37
|
+
const data = fs.readFileSync(filePath, "utf-8");
|
|
38
|
+
const parsed = JSON.parse(data);
|
|
39
|
+
if (typeof parsed.x === "number" &&
|
|
40
|
+
typeof parsed.y === "number" &&
|
|
41
|
+
Number.isFinite(parsed.x) &&
|
|
42
|
+
Number.isFinite(parsed.y)) {
|
|
43
|
+
return { x: parsed.x, y: parsed.y };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// File doesn't exist or is invalid
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function readLegacyCamera() {
|
|
52
|
+
const filePath = path.join(getOpenclawDir(), CAMERA_ZOOM_FILE);
|
|
53
|
+
try {
|
|
54
|
+
const data = fs.readFileSync(filePath, "utf-8");
|
|
55
|
+
const parsed = JSON.parse(data);
|
|
56
|
+
if (typeof parsed.zoom === "number" &&
|
|
57
|
+
Number.isFinite(parsed.zoom) &&
|
|
58
|
+
parsed.zoom >= CAMERA_ZOOM_MIN &&
|
|
59
|
+
parsed.zoom <= CAMERA_ZOOM_MAX) {
|
|
60
|
+
return { zoom: parsed.zoom };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// File doesn't exist or is invalid
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function deleteLegacyFile(filename) {
|
|
69
|
+
const filePath = path.join(getOpenclawDir(), filename);
|
|
70
|
+
try {
|
|
71
|
+
fs.unlinkSync(filePath);
|
|
72
|
+
console.log(`[migrations] Deleted legacy file: ${filename}`);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// File doesn't exist or can't be deleted
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Migrates legacy separate files (position.json, camera.json) to unified settings.
|
|
80
|
+
* Returns the migrated settings if migration occurred, null otherwise.
|
|
81
|
+
* Safe to call multiple times - only migrates if legacy files exist.
|
|
82
|
+
*/
|
|
83
|
+
export function migrateLegacyFiles() {
|
|
84
|
+
const legacyPosition = readLegacyPosition();
|
|
85
|
+
const legacyCamera = readLegacyCamera();
|
|
86
|
+
// No legacy files to migrate
|
|
87
|
+
if (!legacyPosition && !legacyCamera) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
console.log("[migrations] Found legacy settings files, migrating...");
|
|
91
|
+
// Load current settings (may be defaults if new install)
|
|
92
|
+
const current = loadSettings();
|
|
93
|
+
// Build migrated settings
|
|
94
|
+
const migrated = {
|
|
95
|
+
...createDefaultSettings(),
|
|
96
|
+
...current,
|
|
97
|
+
schemaVersion: SETTINGS_SCHEMA_VERSION,
|
|
98
|
+
};
|
|
99
|
+
// Only migrate position if not already set in new format
|
|
100
|
+
if (legacyPosition && (!current.position || Object.keys(current.position).length === 0)) {
|
|
101
|
+
const hash = computeDisplayHash();
|
|
102
|
+
migrated.position = { [hash]: legacyPosition };
|
|
103
|
+
console.log(`[migrations] Migrated position: (${legacyPosition.x}, ${legacyPosition.y}) for display ${hash}`);
|
|
104
|
+
}
|
|
105
|
+
// Only migrate camera if not already set in new format
|
|
106
|
+
if (legacyCamera && !current.camera) {
|
|
107
|
+
migrated.camera = { zoom: legacyCamera.zoom };
|
|
108
|
+
console.log(`[migrations] Migrated camera zoom: ${legacyCamera.zoom}`);
|
|
109
|
+
}
|
|
110
|
+
// Save migrated settings
|
|
111
|
+
getSettingsStore().save(migrated);
|
|
112
|
+
// Delete legacy files after successful migration
|
|
113
|
+
if (legacyPosition) {
|
|
114
|
+
deleteLegacyFile(WINDOW_POSITION_FILE);
|
|
115
|
+
}
|
|
116
|
+
if (legacyCamera) {
|
|
117
|
+
deleteLegacyFile(CAMERA_ZOOM_FILE);
|
|
118
|
+
}
|
|
119
|
+
console.log("[migrations] Migration complete");
|
|
120
|
+
return migrated;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Migrates settings from schema v1 to v2.
|
|
124
|
+
* Converts position from `{x,y}` flat object to `Record<displayHash, {x,y}>`.
|
|
125
|
+
* Reads raw JSON to avoid schema validation rejecting the old format.
|
|
126
|
+
*/
|
|
127
|
+
export function migrateV1ToV2() {
|
|
128
|
+
const filePath = path.join(getOpenclawDir(), "flawed-avatar-settings.json");
|
|
129
|
+
let raw;
|
|
130
|
+
try {
|
|
131
|
+
raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return; // No settings file yet
|
|
135
|
+
}
|
|
136
|
+
// Already at v2 or newer
|
|
137
|
+
if (typeof raw.schemaVersion === "number" && raw.schemaVersion >= 2)
|
|
138
|
+
return;
|
|
139
|
+
// Check if position is old v1 format ({x, y} directly)
|
|
140
|
+
const pos = raw.position;
|
|
141
|
+
if (pos &&
|
|
142
|
+
typeof pos === "object" &&
|
|
143
|
+
!Array.isArray(pos) &&
|
|
144
|
+
"x" in pos &&
|
|
145
|
+
"y" in pos) {
|
|
146
|
+
const oldPos = pos;
|
|
147
|
+
const hash = computeDisplayHash();
|
|
148
|
+
console.log(`[migrations] Converting v1 position to v2 keyed by display hash ${hash}`);
|
|
149
|
+
raw.position = { [hash]: { x: oldPos.x, y: oldPos.y } };
|
|
150
|
+
}
|
|
151
|
+
raw.schemaVersion = 2;
|
|
152
|
+
try {
|
|
153
|
+
fs.writeFileSync(filePath, JSON.stringify(raw, null, "\t"));
|
|
154
|
+
console.log("[migrations] Schema upgraded to v2");
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
console.warn("[migrations] Failed to write v2 migration:", err);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Migrates settings from schema v2 to v3.
|
|
162
|
+
* Adds `scale` and `lightingProfile` fields with defaults.
|
|
163
|
+
*/
|
|
164
|
+
export function migrateV2ToV3() {
|
|
165
|
+
const filePath = path.join(getOpenclawDir(), "flawed-avatar-settings.json");
|
|
166
|
+
let raw;
|
|
167
|
+
try {
|
|
168
|
+
raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return; // No settings file yet
|
|
172
|
+
}
|
|
173
|
+
// Already at v3 or newer
|
|
174
|
+
if (typeof raw.schemaVersion === "number" && raw.schemaVersion >= 3)
|
|
175
|
+
return;
|
|
176
|
+
// Add defaults for new fields
|
|
177
|
+
if (raw.scale === undefined) {
|
|
178
|
+
raw.scale = SCALE_DEFAULT;
|
|
179
|
+
}
|
|
180
|
+
if (raw.lightingProfile === undefined) {
|
|
181
|
+
raw.lightingProfile = LIGHTING_PROFILE_DEFAULT;
|
|
182
|
+
}
|
|
183
|
+
raw.schemaVersion = SETTINGS_SCHEMA_VERSION;
|
|
184
|
+
try {
|
|
185
|
+
fs.writeFileSync(filePath, JSON.stringify(raw, null, "\t"));
|
|
186
|
+
console.log("[migrations] Schema upgraded to v3");
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
console.warn("[migrations] Failed to write v3 migration:", err);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type FileStore } from "./file-store.js";
|
|
2
|
+
import { type Settings, type LightingCustom } from "./types.js";
|
|
3
|
+
export declare function loadSettings(): Settings;
|
|
4
|
+
export declare function saveSettings(settings: Partial<Settings>): void;
|
|
5
|
+
export declare function savePosition(x: number, y: number): void;
|
|
6
|
+
export declare function saveZoom(zoom: number): void;
|
|
7
|
+
export declare function saveOpacity(opacity: number): void;
|
|
8
|
+
export declare function saveIdleTimeout(ms: number): void;
|
|
9
|
+
export declare function getPosition(): {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
} | null;
|
|
13
|
+
export declare function getZoom(): number;
|
|
14
|
+
export declare function getOpacity(): number;
|
|
15
|
+
export declare function getIdleTimeout(): number;
|
|
16
|
+
export declare function saveTtsEnabled(enabled: boolean): void;
|
|
17
|
+
export declare function getTtsEnabled(): boolean;
|
|
18
|
+
export declare function saveTtsEngine(engine: "web-speech" | "kokoro"): void;
|
|
19
|
+
export declare function getTtsEngine(): "web-speech" | "kokoro";
|
|
20
|
+
export declare function saveTtsVoice(voice: string): void;
|
|
21
|
+
export declare function getTtsVoice(): string;
|
|
22
|
+
export declare function saveVrmModelPath(path: string): void;
|
|
23
|
+
export declare function getVrmModelPath(): string | undefined;
|
|
24
|
+
export declare function saveScale(scale: number): void;
|
|
25
|
+
export declare function getScale(): number;
|
|
26
|
+
export declare function saveLightingProfile(profile: string): void;
|
|
27
|
+
export declare function getLightingProfile(): string;
|
|
28
|
+
export declare function saveLightingCustom(custom: LightingCustom): void;
|
|
29
|
+
export declare function getLightingCustom(): LightingCustom | undefined;
|
|
30
|
+
export declare function flushSettings(): Promise<void>;
|
|
31
|
+
export declare function cleanupSettings(): void;
|
|
32
|
+
export declare function getSettingsStore(): FileStore<Settings>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { SETTINGS_FILE, CAMERA_ZOOM_MIN, CAMERA_ZOOM_MAX, CAMERA_ZOOM_DEFAULT, OPACITY_MIN, OPACITY_MAX, OPACITY_DEFAULT, IDLE_TIMEOUT_DEFAULT, SETTINGS_DEBOUNCE_MS, TTS_ENABLED_DEFAULT, TTS_ENGINE_DEFAULT, TTS_VOICE_DEFAULT, SCALE_MIN, SCALE_MAX, SCALE_DEFAULT, LIGHTING_PROFILE_DEFAULT, } from "../../shared/config.js";
|
|
2
|
+
import { createFileStore } from "./file-store.js";
|
|
3
|
+
import { SettingsSchema, createDefaultSettings, } from "./types.js";
|
|
4
|
+
import { computeDisplayHash } from "../display-utils.js";
|
|
5
|
+
let store = null;
|
|
6
|
+
function getStore() {
|
|
7
|
+
if (!store) {
|
|
8
|
+
store = createFileStore({
|
|
9
|
+
filename: SETTINGS_FILE,
|
|
10
|
+
schema: SettingsSchema,
|
|
11
|
+
defaultValue: createDefaultSettings,
|
|
12
|
+
debounceMs: SETTINGS_DEBOUNCE_MS,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return store;
|
|
16
|
+
}
|
|
17
|
+
export function loadSettings() {
|
|
18
|
+
const result = getStore().load();
|
|
19
|
+
return result.ok ? result.data : result.fallback;
|
|
20
|
+
}
|
|
21
|
+
export function saveSettings(settings) {
|
|
22
|
+
const current = getStore().getCache() ?? loadSettings();
|
|
23
|
+
const updated = { ...current, ...settings };
|
|
24
|
+
getStore().save(updated);
|
|
25
|
+
}
|
|
26
|
+
export function savePosition(x, y) {
|
|
27
|
+
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
28
|
+
return;
|
|
29
|
+
const hash = computeDisplayHash();
|
|
30
|
+
const current = getStore().getCache() ?? loadSettings();
|
|
31
|
+
const positions = { ...current.position };
|
|
32
|
+
positions[hash] = { x: Math.round(x), y: Math.round(y) };
|
|
33
|
+
const updated = { ...current, position: positions };
|
|
34
|
+
getStore().save(updated);
|
|
35
|
+
}
|
|
36
|
+
export function saveZoom(zoom) {
|
|
37
|
+
if (!Number.isFinite(zoom))
|
|
38
|
+
return;
|
|
39
|
+
const clamped = Math.max(CAMERA_ZOOM_MIN, Math.min(CAMERA_ZOOM_MAX, zoom));
|
|
40
|
+
const current = getStore().getCache() ?? loadSettings();
|
|
41
|
+
const updated = {
|
|
42
|
+
...current,
|
|
43
|
+
camera: { zoom: clamped },
|
|
44
|
+
};
|
|
45
|
+
getStore().save(updated);
|
|
46
|
+
}
|
|
47
|
+
export function saveOpacity(opacity) {
|
|
48
|
+
if (!Number.isFinite(opacity))
|
|
49
|
+
return;
|
|
50
|
+
const clamped = Math.max(OPACITY_MIN, Math.min(OPACITY_MAX, opacity));
|
|
51
|
+
const current = getStore().getCache() ?? loadSettings();
|
|
52
|
+
const updated = {
|
|
53
|
+
...current,
|
|
54
|
+
opacity: clamped,
|
|
55
|
+
};
|
|
56
|
+
getStore().save(updated);
|
|
57
|
+
}
|
|
58
|
+
export function saveIdleTimeout(ms) {
|
|
59
|
+
if (!Number.isInteger(ms) || ms < 0)
|
|
60
|
+
return;
|
|
61
|
+
const current = getStore().getCache() ?? loadSettings();
|
|
62
|
+
const updated = {
|
|
63
|
+
...current,
|
|
64
|
+
idleTimeoutMs: ms,
|
|
65
|
+
};
|
|
66
|
+
getStore().save(updated);
|
|
67
|
+
}
|
|
68
|
+
export function getPosition() {
|
|
69
|
+
const settings = getStore().getCache() ?? loadSettings();
|
|
70
|
+
if (!settings.position)
|
|
71
|
+
return null;
|
|
72
|
+
const hash = computeDisplayHash();
|
|
73
|
+
return settings.position[hash] ?? null;
|
|
74
|
+
}
|
|
75
|
+
export function getZoom() {
|
|
76
|
+
const settings = getStore().getCache() ?? loadSettings();
|
|
77
|
+
return settings.camera?.zoom ?? CAMERA_ZOOM_DEFAULT;
|
|
78
|
+
}
|
|
79
|
+
export function getOpacity() {
|
|
80
|
+
const settings = getStore().getCache() ?? loadSettings();
|
|
81
|
+
return settings.opacity ?? OPACITY_DEFAULT;
|
|
82
|
+
}
|
|
83
|
+
export function getIdleTimeout() {
|
|
84
|
+
const settings = getStore().getCache() ?? loadSettings();
|
|
85
|
+
return settings.idleTimeoutMs ?? IDLE_TIMEOUT_DEFAULT;
|
|
86
|
+
}
|
|
87
|
+
export function saveTtsEnabled(enabled) {
|
|
88
|
+
const current = getStore().getCache() ?? loadSettings();
|
|
89
|
+
const updated = {
|
|
90
|
+
...current,
|
|
91
|
+
ttsEnabled: enabled,
|
|
92
|
+
};
|
|
93
|
+
getStore().save(updated);
|
|
94
|
+
}
|
|
95
|
+
export function getTtsEnabled() {
|
|
96
|
+
const settings = getStore().getCache() ?? loadSettings();
|
|
97
|
+
return settings.ttsEnabled ?? TTS_ENABLED_DEFAULT;
|
|
98
|
+
}
|
|
99
|
+
export function saveTtsEngine(engine) {
|
|
100
|
+
const current = getStore().getCache() ?? loadSettings();
|
|
101
|
+
const updated = {
|
|
102
|
+
...current,
|
|
103
|
+
ttsEngine: engine,
|
|
104
|
+
};
|
|
105
|
+
getStore().save(updated);
|
|
106
|
+
}
|
|
107
|
+
export function getTtsEngine() {
|
|
108
|
+
const settings = getStore().getCache() ?? loadSettings();
|
|
109
|
+
return settings.ttsEngine ?? TTS_ENGINE_DEFAULT;
|
|
110
|
+
}
|
|
111
|
+
export function saveTtsVoice(voice) {
|
|
112
|
+
const current = getStore().getCache() ?? loadSettings();
|
|
113
|
+
const updated = {
|
|
114
|
+
...current,
|
|
115
|
+
ttsVoice: voice,
|
|
116
|
+
};
|
|
117
|
+
getStore().save(updated);
|
|
118
|
+
}
|
|
119
|
+
export function getTtsVoice() {
|
|
120
|
+
const settings = getStore().getCache() ?? loadSettings();
|
|
121
|
+
return settings.ttsVoice ?? TTS_VOICE_DEFAULT;
|
|
122
|
+
}
|
|
123
|
+
export function saveVrmModelPath(path) {
|
|
124
|
+
const current = getStore().getCache() ?? loadSettings();
|
|
125
|
+
const updated = {
|
|
126
|
+
...current,
|
|
127
|
+
vrmModelPath: path,
|
|
128
|
+
};
|
|
129
|
+
getStore().save(updated);
|
|
130
|
+
}
|
|
131
|
+
export function getVrmModelPath() {
|
|
132
|
+
const settings = getStore().getCache() ?? loadSettings();
|
|
133
|
+
return settings.vrmModelPath;
|
|
134
|
+
}
|
|
135
|
+
export function saveScale(scale) {
|
|
136
|
+
if (!Number.isFinite(scale))
|
|
137
|
+
return;
|
|
138
|
+
const clamped = Math.max(SCALE_MIN, Math.min(SCALE_MAX, scale));
|
|
139
|
+
const current = getStore().getCache() ?? loadSettings();
|
|
140
|
+
const updated = { ...current, scale: clamped };
|
|
141
|
+
getStore().save(updated);
|
|
142
|
+
}
|
|
143
|
+
export function getScale() {
|
|
144
|
+
const settings = getStore().getCache() ?? loadSettings();
|
|
145
|
+
return settings.scale ?? SCALE_DEFAULT;
|
|
146
|
+
}
|
|
147
|
+
export function saveLightingProfile(profile) {
|
|
148
|
+
const current = getStore().getCache() ?? loadSettings();
|
|
149
|
+
const updated = { ...current, lightingProfile: profile };
|
|
150
|
+
getStore().save(updated);
|
|
151
|
+
}
|
|
152
|
+
export function getLightingProfile() {
|
|
153
|
+
const settings = getStore().getCache() ?? loadSettings();
|
|
154
|
+
return settings.lightingProfile ?? LIGHTING_PROFILE_DEFAULT;
|
|
155
|
+
}
|
|
156
|
+
export function saveLightingCustom(custom) {
|
|
157
|
+
const current = getStore().getCache() ?? loadSettings();
|
|
158
|
+
const updated = { ...current, lightingCustom: custom };
|
|
159
|
+
getStore().save(updated);
|
|
160
|
+
}
|
|
161
|
+
export function getLightingCustom() {
|
|
162
|
+
const settings = getStore().getCache() ?? loadSettings();
|
|
163
|
+
return settings.lightingCustom;
|
|
164
|
+
}
|
|
165
|
+
export async function flushSettings() {
|
|
166
|
+
await getStore().flush();
|
|
167
|
+
}
|
|
168
|
+
export function cleanupSettings() {
|
|
169
|
+
getStore().cleanup();
|
|
170
|
+
}
|
|
171
|
+
// For direct store access (e.g., migrations)
|
|
172
|
+
export function getSettingsStore() {
|
|
173
|
+
return getStore();
|
|
174
|
+
}
|