flawed-avatar 0.2.1 → 0.2.2
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/LICENSE +21 -0
- package/README.md +217 -0
- package/assets/icon.png +0 -0
- package/dist/chat-renderer-bundle/chat-index.html +1 -1
- package/dist/main/main/device-identity.d.ts +19 -0
- package/dist/main/main/device-identity.js +83 -0
- package/dist/main/main/gateway-client.d.ts +2 -1
- package/dist/main/main/gateway-client.js +50 -12
- package/dist/main/main/main.js +5 -7
- package/dist/main/main/persistence/types.d.ts +2 -2
- package/dist/renderer-bundle/renderer.js +35 -46
- package/dist/settings-preload.cjs +153 -0
- package/dist/settings-renderer-bundle/settings-index.html +16 -0
- package/dist/settings-renderer-bundle/settings-renderer.js +502 -0
- package/dist/settings-renderer-bundle/styles/base.css +106 -0
- package/dist/settings-renderer-bundle/styles/chat.css +516 -0
- package/dist/settings-renderer-bundle/styles/components/button.css +221 -0
- package/dist/settings-renderer-bundle/styles/components/indicator.css +216 -0
- package/dist/settings-renderer-bundle/styles/components/input.css +139 -0
- package/dist/settings-renderer-bundle/styles/components/toast.css +204 -0
- package/dist/settings-renderer-bundle/styles/controls.css +279 -0
- package/dist/settings-renderer-bundle/styles/settings.css +310 -0
- package/dist/settings-renderer-bundle/styles/tokens.css +220 -0
- package/dist/settings-renderer-bundle/styles/utilities.css +349 -0
- package/index.ts +2 -2
- package/package.json +6 -1
- package/src/main/device-identity.ts +103 -0
- package/src/main/gateway-client.ts +52 -11
- package/src/main/main.ts +5 -6
- package/src/renderer/audio/index.ts +0 -3
- package/src/renderer/audio/kokoro-model-loader.ts +0 -2
- package/src/renderer/audio/kokoro-tts-service.ts +0 -2
- package/src/renderer/audio/tts-controller.ts +0 -3
- package/src/renderer/avatar/ibl-enhancer.ts +1 -1
- package/src/renderer/chat-window/chat-index.html +1 -1
- package/src/renderer/renderer.ts +0 -1
- package/src/renderer/settings-window/settings-index.html +1 -1
- package/src/renderer/ui/chat-bubble.ts +0 -39
- package/src/service.ts +1 -1
- package/src/renderer/audio/tts-service.ts +0 -16
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
3
6
|
import { GATEWAY_RECONNECT_BASE_MS, GATEWAY_RECONNECT_MAX_MS } from "../shared/config.js";
|
|
4
7
|
import type { AgentState } from "../shared/types.js";
|
|
8
|
+
import type { DeviceIdentity } from "./device-identity.js";
|
|
9
|
+
import { loadStoredAuthToken, buildAuthPayload, signPayload, publicKeyToBase64Url } from "./device-identity.js";
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
const PKG_VERSION: string = (() => {
|
|
14
|
+
try {
|
|
15
|
+
const raw = fs.readFileSync(path.resolve(__dirname, "..", "..", "..", "package.json"), "utf-8");
|
|
16
|
+
return (JSON.parse(raw) as { version: string }).version;
|
|
17
|
+
} catch {
|
|
18
|
+
return "0.0.0";
|
|
19
|
+
}
|
|
20
|
+
})();
|
|
5
21
|
|
|
6
22
|
const PROTOCOL_VERSION = 3;
|
|
7
23
|
|
|
@@ -40,6 +56,7 @@ export function createGatewayClient(
|
|
|
40
56
|
onModelSwitch: (vrmPath: string) => void,
|
|
41
57
|
agentConfigs?: Record<string, { vrmPath?: string }>,
|
|
42
58
|
authToken?: string,
|
|
59
|
+
deviceIdentity?: DeviceIdentity | null,
|
|
43
60
|
): { destroy: () => void; sendChat: (text: string, sessionKey: string | null) => void; getCurrentAgentId: () => string | null } {
|
|
44
61
|
let ws: WebSocket | null = null;
|
|
45
62
|
let destroyed = false;
|
|
@@ -60,7 +77,6 @@ export function createGatewayClient(
|
|
|
60
77
|
|
|
61
78
|
// Track session changes - agent events contain the actual sessionKey
|
|
62
79
|
if (sessionKey && sessionKey !== currentSessionKey) {
|
|
63
|
-
console.log("flawed-avatar: setting currentSessionKey from agent event:", sessionKey);
|
|
64
80
|
currentSessionKey = sessionKey;
|
|
65
81
|
if (agentConfigs?.[sessionKey]?.vrmPath) {
|
|
66
82
|
onModelSwitch(agentConfigs[sessionKey].vrmPath!);
|
|
@@ -124,7 +140,6 @@ export function createGatewayClient(
|
|
|
124
140
|
const firstSession = sessions[0];
|
|
125
141
|
if (firstSession.key) {
|
|
126
142
|
currentSessionKey = firstSession.key;
|
|
127
|
-
console.log("flawed-avatar: auto-detected session from sessions.list:", currentSessionKey, "displayName:", firstSession.displayName);
|
|
128
143
|
}
|
|
129
144
|
}
|
|
130
145
|
}
|
|
@@ -139,14 +154,12 @@ export function createGatewayClient(
|
|
|
139
154
|
const agentId = firstAgent.id ?? "main";
|
|
140
155
|
const sessionKey = `agent:${agentId}:main`;
|
|
141
156
|
currentSessionKey = sessionKey;
|
|
142
|
-
console.log("flawed-avatar: fallback to agent main session:", currentSessionKey);
|
|
143
157
|
}
|
|
144
158
|
}
|
|
145
159
|
|
|
146
160
|
// Connect success - request active sessions once
|
|
147
161
|
if (connectSent && !connectionSetupDone && !sessionsListRequestId && !agentsListRequestId) {
|
|
148
162
|
connectionSetupDone = true;
|
|
149
|
-
console.log("flawed-avatar: gateway connected successfully");
|
|
150
163
|
backoffMs = GATEWAY_RECONNECT_BASE_MS;
|
|
151
164
|
// Request recently active sessions
|
|
152
165
|
requestSessionsList();
|
|
@@ -176,6 +189,36 @@ export function createGatewayClient(
|
|
|
176
189
|
connectTimer = null;
|
|
177
190
|
}
|
|
178
191
|
|
|
192
|
+
const role = "operator";
|
|
193
|
+
const scopes = ["operator.admin"];
|
|
194
|
+
const storedToken = deviceIdentity ? loadStoredAuthToken(deviceIdentity.deviceId, role) : null;
|
|
195
|
+
const effectiveToken = storedToken ?? authToken ?? undefined;
|
|
196
|
+
const auth = effectiveToken ? { token: effectiveToken } : undefined;
|
|
197
|
+
|
|
198
|
+
const nonce = connectNonce ?? undefined;
|
|
199
|
+
const signedAtMs = Date.now();
|
|
200
|
+
const device = (() => {
|
|
201
|
+
if (!deviceIdentity) return undefined;
|
|
202
|
+
const payload = buildAuthPayload({
|
|
203
|
+
deviceId: deviceIdentity.deviceId,
|
|
204
|
+
clientId: "gateway-client",
|
|
205
|
+
clientMode: "backend",
|
|
206
|
+
role,
|
|
207
|
+
scopes,
|
|
208
|
+
signedAtMs,
|
|
209
|
+
token: effectiveToken ?? null,
|
|
210
|
+
nonce,
|
|
211
|
+
});
|
|
212
|
+
const signature = signPayload(deviceIdentity.privateKeyPem, payload);
|
|
213
|
+
return {
|
|
214
|
+
id: deviceIdentity.deviceId,
|
|
215
|
+
publicKey: publicKeyToBase64Url(deviceIdentity.publicKeyPem),
|
|
216
|
+
signature,
|
|
217
|
+
signedAt: signedAtMs,
|
|
218
|
+
nonce,
|
|
219
|
+
};
|
|
220
|
+
})();
|
|
221
|
+
|
|
179
222
|
const frame = {
|
|
180
223
|
type: "req",
|
|
181
224
|
id: randomUUID(),
|
|
@@ -186,14 +229,15 @@ export function createGatewayClient(
|
|
|
186
229
|
client: {
|
|
187
230
|
id: "gateway-client",
|
|
188
231
|
displayName: "Flawed Avatar",
|
|
189
|
-
version:
|
|
232
|
+
version: PKG_VERSION,
|
|
190
233
|
platform: process.platform,
|
|
191
234
|
mode: "backend",
|
|
192
235
|
},
|
|
193
236
|
caps: [],
|
|
194
|
-
role
|
|
195
|
-
scopes
|
|
196
|
-
auth
|
|
237
|
+
role,
|
|
238
|
+
scopes,
|
|
239
|
+
auth,
|
|
240
|
+
device,
|
|
197
241
|
},
|
|
198
242
|
};
|
|
199
243
|
|
|
@@ -223,7 +267,6 @@ export function createGatewayClient(
|
|
|
223
267
|
limit: 10,
|
|
224
268
|
},
|
|
225
269
|
};
|
|
226
|
-
console.log("flawed-avatar: requesting sessions list");
|
|
227
270
|
ws.send(JSON.stringify(frame));
|
|
228
271
|
}
|
|
229
272
|
|
|
@@ -236,7 +279,6 @@ export function createGatewayClient(
|
|
|
236
279
|
method: "agents.list",
|
|
237
280
|
params: {},
|
|
238
281
|
};
|
|
239
|
-
console.log("flawed-avatar: requesting agents list (fallback)");
|
|
240
282
|
ws.send(JSON.stringify(frame));
|
|
241
283
|
}
|
|
242
284
|
|
|
@@ -246,7 +288,6 @@ export function createGatewayClient(
|
|
|
246
288
|
ws = new WebSocket(gatewayUrl, { maxPayload: 25 * 1024 * 1024 });
|
|
247
289
|
|
|
248
290
|
ws.on("open", () => {
|
|
249
|
-
console.log("flawed-avatar: ws open, sending connect frame");
|
|
250
291
|
queueConnect();
|
|
251
292
|
});
|
|
252
293
|
|
package/src/main/main.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { createWindowManager } from "./window-manager.js";
|
|
|
7
7
|
import { createTray } from "./tray.js";
|
|
8
8
|
import { createStdinListener, type StdinCommand } from "./stdin-listener.js";
|
|
9
9
|
import { createGatewayClient } from "./gateway-client.js";
|
|
10
|
+
import { loadDeviceIdentity } from "./device-identity.js";
|
|
10
11
|
import { IPC } from "../shared/ipc-channels.js";
|
|
11
12
|
import { GATEWAY_URL_DEFAULT, CHAT_INPUT_MAX_LENGTH } from "../shared/config.js";
|
|
12
13
|
import { getVrmModelPath, saveVrmModelPath } from "./persistence/index.js";
|
|
@@ -77,7 +78,7 @@ app.whenReady().then(() => {
|
|
|
77
78
|
createTray(wm);
|
|
78
79
|
|
|
79
80
|
// Return VRM model path (CLI override > persisted > default)
|
|
80
|
-
const defaultVrmPath = path.join(__dirname, "..", "..", "..", "assets", "models", "
|
|
81
|
+
const defaultVrmPath = path.join(__dirname, "..", "..", "..", "assets", "models", "CaptainLobster.vrm");
|
|
81
82
|
ipcMain.handle(IPC.GET_VRM_PATH, () => {
|
|
82
83
|
if (cliVrmPath) return cliVrmPath;
|
|
83
84
|
const persisted = getVrmModelPath();
|
|
@@ -133,25 +134,23 @@ app.whenReady().then(() => {
|
|
|
133
134
|
// Connect to gateway WebSocket for agent event streaming
|
|
134
135
|
const gatewayUrl = cliGatewayUrl ?? GATEWAY_URL_DEFAULT;
|
|
135
136
|
const authToken = resolveAuthToken();
|
|
136
|
-
|
|
137
|
+
const deviceIdentity = loadDeviceIdentity();
|
|
138
|
+
console.log(`flawed-avatar: connecting to ${gatewayUrl} (auth=${authToken ? "token" : "none"}, device=${deviceIdentity ? deviceIdentity.deviceId.slice(0, 8) + "…" : "none"})`);
|
|
137
139
|
const gw = createGatewayClient(
|
|
138
140
|
gatewayUrl,
|
|
139
141
|
(state) => wm.sendAgentState(state),
|
|
140
142
|
(vrmPath) => wm.sendToAvatar(IPC.VRM_MODEL_CHANGED, vrmPath),
|
|
141
143
|
agentConfigs,
|
|
142
144
|
authToken,
|
|
145
|
+
deviceIdentity,
|
|
143
146
|
);
|
|
144
147
|
|
|
145
148
|
// IPC: send chat message to active agent
|
|
146
149
|
ipcMain.on(IPC.SEND_CHAT, (_event, text: unknown) => {
|
|
147
|
-
console.log("flawed-avatar: SEND_CHAT received:", text);
|
|
148
150
|
if (typeof text !== "string" || text.trim().length === 0 || text.length > CHAT_INPUT_MAX_LENGTH) {
|
|
149
|
-
console.log("flawed-avatar: SEND_CHAT rejected (validation failed)");
|
|
150
151
|
return;
|
|
151
152
|
}
|
|
152
153
|
const agentId = gw.getCurrentAgentId();
|
|
153
|
-
console.log("flawed-avatar: current agentId:", agentId);
|
|
154
|
-
console.log("flawed-avatar: sending chat to gateway");
|
|
155
154
|
gw.sendChat(text.trim(), agentId);
|
|
156
155
|
});
|
|
157
156
|
|
|
@@ -26,9 +26,6 @@ export {
|
|
|
26
26
|
export { createWebSpeechTTSService } from "./web-speech-tts.js";
|
|
27
27
|
export { createKokoroTTSService, disposeKokoroLoader } from "./kokoro-tts-service.js";
|
|
28
28
|
|
|
29
|
-
// Backwards compatibility alias
|
|
30
|
-
export { createWebSpeechTTSService as createTTSService } from "./web-speech-tts.js";
|
|
31
|
-
|
|
32
29
|
// Factory
|
|
33
30
|
export { createTTSServiceFactory, createTTSServiceWithFallback, type TTSServiceFactory } from "./tts-service-factory.js";
|
|
34
31
|
|
|
@@ -44,7 +44,6 @@ function createModelLoader(): KokoroModelLoader {
|
|
|
44
44
|
throw new Error("Model loader has been disposed");
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
console.log("[Kokoro] Loading model...");
|
|
48
47
|
loading = true;
|
|
49
48
|
|
|
50
49
|
try {
|
|
@@ -57,7 +56,6 @@ function createModelLoader(): KokoroModelLoader {
|
|
|
57
56
|
{ dtype: "q8" }
|
|
58
57
|
);
|
|
59
58
|
|
|
60
|
-
console.log("[Kokoro] Model loaded successfully");
|
|
61
59
|
model = tts as unknown as KokoroTTS;
|
|
62
60
|
loading = false;
|
|
63
61
|
return model;
|
|
@@ -166,8 +166,6 @@ export function createKokoroTTSService(events: TTSEvents): TTSService {
|
|
|
166
166
|
return;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
console.log(`[Kokoro] Generating: "${segment.text.slice(0, 40)}..." (ready=${readyAudio.length}, pending=${pendingSegments.length})`);
|
|
170
|
-
|
|
171
169
|
const result = await model.generate(segment.text, { voice: currentVoice });
|
|
172
170
|
|
|
173
171
|
if (disposed || segment.cancelled) {
|
|
@@ -132,7 +132,6 @@ export function createTTSController(
|
|
|
132
132
|
}
|
|
133
133
|
wlipSync = analyzer;
|
|
134
134
|
player.setAnalysisNode(analyzer.node);
|
|
135
|
-
console.log("[TTS] wLipSync analyzer initialized");
|
|
136
135
|
}).catch((err) => {
|
|
137
136
|
console.warn("[TTS] Failed to initialize wLipSync:", err);
|
|
138
137
|
}).finally(() => {
|
|
@@ -271,8 +270,6 @@ export function createTTSController(
|
|
|
271
270
|
setEngine(engine: TTSEngineType): void {
|
|
272
271
|
if (disposed || engine === currentEngine) return;
|
|
273
272
|
|
|
274
|
-
console.log(`[TTS] Switching engine from ${currentEngine} to ${engine}`);
|
|
275
|
-
|
|
276
273
|
// Cancel and dispose current service
|
|
277
274
|
if (ttsService) {
|
|
278
275
|
ttsService.cancel();
|
|
@@ -68,7 +68,7 @@ export function createIBLEnhancer(): IBLEnhancer {
|
|
|
68
68
|
if (!(mat as any).isShaderMaterial && !(mat as any).isMeshStandardMaterial) return;
|
|
69
69
|
if ((mat as any).__iblEnhanced) return; // Guard: already enhanced
|
|
70
70
|
|
|
71
|
-
// Chain onBeforeCompile
|
|
71
|
+
// Chain onBeforeCompile — preserve MToon's existing hook
|
|
72
72
|
const prev = mat.onBeforeCompile;
|
|
73
73
|
mat.onBeforeCompile = (shader, renderer) => {
|
|
74
74
|
prev?.call(mat, shader, renderer);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self' file: data: blob:; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' file: data: blob:; connect-src 'self' file: data: blob:" />
|
|
6
|
-
<title>
|
|
6
|
+
<title>Flawed Avatar Chat</title>
|
|
7
7
|
<link rel="stylesheet" href="./styles/chat.css" />
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
package/src/renderer/renderer.ts
CHANGED
|
@@ -122,7 +122,6 @@ async function boot(): Promise<void> {
|
|
|
122
122
|
const ttsEnabled = await bridge.getTtsEnabled();
|
|
123
123
|
const ttsEngine = await bridge.getTtsEngine();
|
|
124
124
|
const ttsVoice = await bridge.getTtsVoice();
|
|
125
|
-
console.log("[TTS] Initializing with:", { ttsEnabled, ttsEngine, ttsVoice });
|
|
126
125
|
ttsController = createTTSController(animator.getLipSync(), {
|
|
127
126
|
enabled: ttsEnabled,
|
|
128
127
|
engine: ttsEngine || "web-speech",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
content="default-src 'self' file: data: blob:; script-src 'self';
|
|
7
7
|
style-src 'self' 'unsafe-inline'; img-src 'self' file: data: blob:;
|
|
8
8
|
connect-src 'self' file: data: blob:" />
|
|
9
|
-
<title>
|
|
9
|
+
<title>Flawed Avatar Settings</title>
|
|
10
10
|
<link rel="stylesheet" href="./styles/settings.css" />
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -182,45 +182,6 @@ export function createChatBubble(
|
|
|
182
182
|
pruneHistory();
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
// --- Render empty state ---
|
|
186
|
-
function renderEmptyState(): HTMLElement {
|
|
187
|
-
const el = document.createElement("div");
|
|
188
|
-
el.className = "chat-empty";
|
|
189
|
-
el.innerHTML = `
|
|
190
|
-
<div class="chat-empty__icon" aria-hidden="true">💬</div>
|
|
191
|
-
<div class="chat-empty__title">No messages yet</div>
|
|
192
|
-
<div class="chat-empty__message">Start a conversation</div>
|
|
193
|
-
`;
|
|
194
|
-
return el;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// --- Render error state ---
|
|
198
|
-
function renderError(message: string): HTMLElement {
|
|
199
|
-
const el = document.createElement("div");
|
|
200
|
-
el.className = "chat-error";
|
|
201
|
-
el.setAttribute("role", "alert");
|
|
202
|
-
el.innerHTML = `
|
|
203
|
-
<svg class="chat-error__icon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
204
|
-
<circle cx="8" cy="8" r="7" fill="none" stroke="currentColor" stroke-width="1.5"/>
|
|
205
|
-
<path d="M8 4v5M8 11v1"/>
|
|
206
|
-
</svg>
|
|
207
|
-
<span class="chat-error__message">${message}</span>
|
|
208
|
-
`;
|
|
209
|
-
return el;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// --- Render loading skeleton ---
|
|
213
|
-
function renderLoadingSkeleton(): HTMLElement {
|
|
214
|
-
const el = document.createElement("div");
|
|
215
|
-
el.className = "chat-loading";
|
|
216
|
-
for (let i = 0; i < 3; i++) {
|
|
217
|
-
const skeleton = document.createElement("div");
|
|
218
|
-
skeleton.className = "skeleton message-skeleton";
|
|
219
|
-
el.appendChild(skeleton);
|
|
220
|
-
}
|
|
221
|
-
return el;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
185
|
// --- Send message ---
|
|
225
186
|
function sendMessage(text: string): void {
|
|
226
187
|
bridge.sendChat(text);
|
package/src/service.ts
CHANGED
|
@@ -13,7 +13,7 @@ const RESTART_BASE_MS = 5_000;
|
|
|
13
13
|
const RESTART_MAX_MS = 30_000;
|
|
14
14
|
const RESTART_RESET_MS = 60_000;
|
|
15
15
|
|
|
16
|
-
export function
|
|
16
|
+
export function createFlawedAvatarService(api: OpenClawPluginApi) {
|
|
17
17
|
let child: ChildProcess | null = null;
|
|
18
18
|
let stopped = false;
|
|
19
19
|
let restartBackoffMs = RESTART_BASE_MS;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TTS Service - Re-exports for backwards compatibility.
|
|
3
|
-
*
|
|
4
|
-
* The implementation has been split into:
|
|
5
|
-
* - types.ts: Interface definitions
|
|
6
|
-
* - web-speech-tts.ts: Web Speech API implementation
|
|
7
|
-
* - kokoro-tts-service.ts: Kokoro.js implementation
|
|
8
|
-
* - tts-service-factory.ts: Factory for creating services
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
// Re-export types for backwards compatibility
|
|
12
|
-
export type { TTSEvents, TTSService, TTSVoice, TTSEngineType, TTSControllerConfig } from "./types.js";
|
|
13
|
-
export { TTS_ENGINES, KOKORO_VOICES, KOKORO_DEFAULT_VOICE } from "./types.js";
|
|
14
|
-
|
|
15
|
-
// Re-export Web Speech implementation as default factory for backwards compatibility
|
|
16
|
-
export { createWebSpeechTTSService as createTTSService } from "./web-speech-tts.js";
|