clawmate 1.3.0 → 1.4.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/electron-builder.yml +1 -1
- package/index.js +589 -406
- package/main/ai-bridge.js +64 -58
- package/main/ai-connector.js +67 -62
- package/main/autostart.js +7 -7
- package/main/desktop-path.js +4 -4
- package/main/file-command-parser.js +77 -41
- package/main/file-ops.js +27 -27
- package/main/index.js +18 -16
- package/main/ipc-handlers.js +27 -24
- package/main/manifest.js +2 -2
- package/main/platform.js +16 -16
- package/main/smart-file-ops.js +64 -64
- package/main/store.js +1 -1
- package/main/telegram.js +154 -121
- package/main/tray.js +226 -71
- package/main/updater.js +13 -13
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -4
- package/preload/preload.js +18 -18
- package/renderer/css/effects.css +6 -6
- package/renderer/css/pet.css +8 -8
- package/renderer/css/speech.css +5 -5
- package/renderer/first-run.html +15 -15
- package/renderer/index.html +4 -4
- package/renderer/js/ai-controller.js +99 -88
- package/renderer/js/app.js +26 -23
- package/renderer/js/browser-watcher.js +32 -32
- package/renderer/js/character.js +33 -33
- package/renderer/js/interactions.js +57 -14
- package/renderer/js/memory.js +144 -37
- package/renderer/js/metrics.js +141 -141
- package/renderer/js/mode-manager.js +59 -15
- package/renderer/js/pet-engine.js +236 -236
- package/renderer/js/speech.js +19 -19
- package/renderer/js/state-machine.js +23 -23
- package/renderer/js/time-aware.js +15 -15
- package/renderer/launcher.html +9 -9
- package/shared/constants.js +11 -11
- package/shared/messages.js +130 -130
- package/shared/personalities.js +72 -37
- package/skills/launch-pet/index.js +13 -13
- package/skills/launch-pet/skill.json +12 -23
package/renderer/js/metrics.js
CHANGED
|
@@ -1,87 +1,87 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Real-time pet behavior quality meter (Self-Observation System)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Collects various metrics on the renderer side so ClawMate can
|
|
5
|
+
* observe and measure its own behavior quality, then sends them to the main process.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* - frameRate:
|
|
9
|
-
* - stateTransitions:
|
|
10
|
-
* - movementSmoothness:
|
|
11
|
-
* - wallContactAccuracy:
|
|
12
|
-
* - interactionResponseTime:
|
|
13
|
-
* - animationFrameConsistency:
|
|
14
|
-
* - idleRatio:
|
|
15
|
-
* - explorationCoverage:
|
|
16
|
-
* - speechFrequency:
|
|
17
|
-
* - userEngagement:
|
|
7
|
+
* Collected metrics:
|
|
8
|
+
* - frameRate: Actual FPS (requestAnimationFrame based)
|
|
9
|
+
* - stateTransitions: State transition count/patterns (last 60 seconds)
|
|
10
|
+
* - movementSmoothness: Movement smoothness (variance of consecutive position changes)
|
|
11
|
+
* - wallContactAccuracy: Wall contact accuracy (edge offset effect)
|
|
12
|
+
* - interactionResponseTime: Click -> response time
|
|
13
|
+
* - animationFrameConsistency: Frame transition consistency
|
|
14
|
+
* - idleRatio: Idle time ratio of total time
|
|
15
|
+
* - explorationCoverage: Screen exploration coverage (visited area ratio)
|
|
16
|
+
* - speechFrequency: Speech bubble frequency
|
|
17
|
+
* - userEngagement: User interaction frequency
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
* -
|
|
21
|
-
* -
|
|
22
|
-
* - 30
|
|
19
|
+
* Performance notes:
|
|
20
|
+
* - Does not directly interfere with requestAnimationFrame loop
|
|
21
|
+
* - Lightweight sampling approach (periodic polling, not per-frame collection)
|
|
22
|
+
* - Summary sent every 30 seconds
|
|
23
23
|
*/
|
|
24
24
|
const Metrics = (() => {
|
|
25
|
-
// ---
|
|
26
|
-
const REPORT_INTERVAL = 30000; //
|
|
27
|
-
const SAMPLE_INTERVAL = 200; //
|
|
28
|
-
const FPS_SAMPLE_INTERVAL = 1000; // FPS
|
|
29
|
-
const GRID_SIZE = 8; //
|
|
30
|
-
const TRANSITION_WINDOW = 60000; //
|
|
31
|
-
|
|
32
|
-
// ---
|
|
25
|
+
// --- Configuration constants ---
|
|
26
|
+
const REPORT_INTERVAL = 30000; // Metric report interval (30s)
|
|
27
|
+
const SAMPLE_INTERVAL = 200; // Position sampling interval (200ms)
|
|
28
|
+
const FPS_SAMPLE_INTERVAL = 1000; // FPS measurement interval (1s)
|
|
29
|
+
const GRID_SIZE = 8; // Exploration coverage grid (8x8 = 64 cells)
|
|
30
|
+
const TRANSITION_WINDOW = 60000; // State transition record retention time (60s)
|
|
31
|
+
|
|
32
|
+
// --- Internal state ---
|
|
33
33
|
let initialized = false;
|
|
34
34
|
let reportTimer = null;
|
|
35
35
|
let sampleTimer = null;
|
|
36
36
|
let fpsRafId = null;
|
|
37
37
|
|
|
38
|
-
// FPS
|
|
38
|
+
// FPS measurement
|
|
39
39
|
let fpsFrameCount = 0;
|
|
40
40
|
let fpsLastTime = 0;
|
|
41
41
|
let currentFps = 60;
|
|
42
|
-
let fpsHistory = []; //
|
|
42
|
+
let fpsHistory = []; // FPS records for the last 30 seconds
|
|
43
43
|
|
|
44
|
-
//
|
|
44
|
+
// State transition tracking
|
|
45
45
|
let stateTransitions = []; // [{ from, to, timestamp }]
|
|
46
46
|
let stateTimeAccum = {}; // { 'idle': totalMs, 'walking': totalMs, ... }
|
|
47
47
|
let lastStateChangeTime = 0;
|
|
48
48
|
let lastObservedState = null;
|
|
49
49
|
|
|
50
|
-
//
|
|
50
|
+
// Movement smoothness measurement
|
|
51
51
|
let positionSamples = []; // [{ x, y, timestamp }]
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
let wallContactSamples = 0; //
|
|
55
|
-
let wallContactAccurateSamples = 0; //
|
|
53
|
+
// Wall contact accuracy
|
|
54
|
+
let wallContactSamples = 0; // Samples during wall contact
|
|
55
|
+
let wallContactAccurateSamples = 0; // Accurate contact samples
|
|
56
56
|
|
|
57
|
-
//
|
|
58
|
-
let lastClickTime = 0; //
|
|
59
|
-
let interactionResponseTimes = []; // [ms]
|
|
57
|
+
// Interaction response time
|
|
58
|
+
let lastClickTime = 0; // Last click timestamp
|
|
59
|
+
let interactionResponseTimes = []; // [ms] Response time records
|
|
60
60
|
|
|
61
|
-
//
|
|
62
|
-
let animFrameTimestamps = []; //
|
|
61
|
+
// Frame transition consistency
|
|
62
|
+
let animFrameTimestamps = []; // Animation frame transition timestamps
|
|
63
63
|
|
|
64
|
-
//
|
|
65
|
-
let visitedGrid = new Set(); //
|
|
64
|
+
// Exploration coverage (8x8 grid)
|
|
65
|
+
let visitedGrid = new Set(); // Visited grid cells (string keys)
|
|
66
66
|
let screenW = 0;
|
|
67
67
|
let screenH = 0;
|
|
68
68
|
|
|
69
|
-
//
|
|
69
|
+
// Speech bubble frequency
|
|
70
70
|
let speechCount = 0;
|
|
71
71
|
|
|
72
|
-
//
|
|
72
|
+
// User interaction frequency
|
|
73
73
|
let userClickCount = 0;
|
|
74
74
|
|
|
75
|
-
//
|
|
75
|
+
// Report period start time
|
|
76
76
|
let periodStartTime = 0;
|
|
77
77
|
|
|
78
78
|
// ===================================
|
|
79
|
-
//
|
|
79
|
+
// Initialization
|
|
80
80
|
// ===================================
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
83
|
+
* Initialize metrics system
|
|
84
|
+
* Observes externally without interfering with existing engine/FSM
|
|
85
85
|
*/
|
|
86
86
|
function init() {
|
|
87
87
|
if (initialized) return;
|
|
@@ -97,34 +97,34 @@ const Metrics = (() => {
|
|
|
97
97
|
screenH = window.innerHeight;
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
// FPS
|
|
100
|
+
// FPS measurement loop (separate rAF -- no interference with existing engine loop)
|
|
101
101
|
_startFpsMeasurement();
|
|
102
102
|
|
|
103
|
-
//
|
|
103
|
+
// Periodic position/state sampling (200ms interval)
|
|
104
104
|
sampleTimer = setInterval(_sampleState, SAMPLE_INTERVAL);
|
|
105
105
|
|
|
106
|
-
// 30
|
|
106
|
+
// Summary report every 30 seconds
|
|
107
107
|
reportTimer = setInterval(_reportSummary, REPORT_INTERVAL);
|
|
108
108
|
|
|
109
|
-
// StateMachine
|
|
109
|
+
// Monitor StateMachine state changes (non-destructive wrapping of existing callback chain)
|
|
110
110
|
_hookStateChanges();
|
|
111
111
|
|
|
112
|
-
//
|
|
112
|
+
// Monitor user click events
|
|
113
113
|
_hookUserInteractions();
|
|
114
114
|
|
|
115
|
-
//
|
|
115
|
+
// Monitor speech bubble events
|
|
116
116
|
_hookSpeechEvents();
|
|
117
117
|
|
|
118
|
-
console.log('[Metrics]
|
|
118
|
+
console.log('[Metrics] Self-observation system initialized');
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
// ===================================
|
|
122
|
-
// FPS
|
|
122
|
+
// FPS Measurement
|
|
123
123
|
// ===================================
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
*
|
|
127
|
-
* PetEngine
|
|
126
|
+
* Measure actual framerate with a separate rAF loop
|
|
127
|
+
* Operates independently from PetEngine's rAF loop
|
|
128
128
|
*/
|
|
129
129
|
function _startFpsMeasurement() {
|
|
130
130
|
fpsFrameCount = 0;
|
|
@@ -133,13 +133,13 @@ const Metrics = (() => {
|
|
|
133
133
|
function fpsLoop(timestamp) {
|
|
134
134
|
fpsFrameCount++;
|
|
135
135
|
|
|
136
|
-
//
|
|
136
|
+
// Calculate FPS every second
|
|
137
137
|
const elapsed = timestamp - fpsLastTime;
|
|
138
138
|
if (elapsed >= FPS_SAMPLE_INTERVAL) {
|
|
139
139
|
currentFps = Math.round((fpsFrameCount / elapsed) * 1000 * 10) / 10;
|
|
140
140
|
fpsHistory.push(currentFps);
|
|
141
141
|
|
|
142
|
-
//
|
|
142
|
+
// Keep last 30 entries (30 seconds)
|
|
143
143
|
if (fpsHistory.length > 30) fpsHistory.shift();
|
|
144
144
|
|
|
145
145
|
fpsFrameCount = 0;
|
|
@@ -153,25 +153,25 @@ const Metrics = (() => {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// ===================================
|
|
156
|
-
//
|
|
156
|
+
// Periodic State Sampling
|
|
157
157
|
// ===================================
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
|
-
*
|
|
161
|
-
* PetEngine
|
|
160
|
+
* Sample pet position/state every 200ms
|
|
161
|
+
* Reads data from PetEngine and StateMachine in read-only mode
|
|
162
162
|
*/
|
|
163
163
|
function _sampleState() {
|
|
164
164
|
const now = Date.now();
|
|
165
165
|
|
|
166
|
-
//
|
|
166
|
+
// Position sampling (for movement smoothness calculation)
|
|
167
167
|
if (typeof PetEngine !== 'undefined') {
|
|
168
168
|
const pos = PetEngine.getPosition();
|
|
169
169
|
positionSamples.push({ x: pos.x, y: pos.y, timestamp: now });
|
|
170
170
|
|
|
171
|
-
//
|
|
171
|
+
// Keep only last 30 seconds worth (150 entries)
|
|
172
172
|
if (positionSamples.length > 150) positionSamples.shift();
|
|
173
173
|
|
|
174
|
-
//
|
|
174
|
+
// Exploration coverage: record current position on grid
|
|
175
175
|
if (screenW > 0 && screenH > 0) {
|
|
176
176
|
const gridX = Math.floor((pos.x / screenW) * GRID_SIZE);
|
|
177
177
|
const gridY = Math.floor((pos.y / screenH) * GRID_SIZE);
|
|
@@ -180,16 +180,16 @@ const Metrics = (() => {
|
|
|
180
180
|
visitedGrid.add(`${clampedX},${clampedY}`);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
//
|
|
183
|
+
// Wall contact accuracy sampling
|
|
184
184
|
if (pos.onSurface && pos.movementMode === 'crawling') {
|
|
185
185
|
wallContactSamples++;
|
|
186
|
-
//
|
|
186
|
+
// Check if accurately flush against edge (based on CHAR_SIZE = 64)
|
|
187
187
|
const charSize = PetEngine.CHAR_SIZE || 64;
|
|
188
188
|
let isAccurate = false;
|
|
189
189
|
|
|
190
190
|
switch (pos.edge) {
|
|
191
191
|
case 'bottom':
|
|
192
|
-
isAccurate = pos.y >= (screenH - charSize - 6); // EDGE_OFFSET(4) + 2
|
|
192
|
+
isAccurate = pos.y >= (screenH - charSize - 6); // EDGE_OFFSET(4) + 2 tolerance
|
|
193
193
|
break;
|
|
194
194
|
case 'top':
|
|
195
195
|
isAccurate = pos.y <= 6;
|
|
@@ -201,18 +201,18 @@ const Metrics = (() => {
|
|
|
201
201
|
isAccurate = pos.x >= (screenW - charSize - 6);
|
|
202
202
|
break;
|
|
203
203
|
case 'surface':
|
|
204
|
-
isAccurate = true; //
|
|
204
|
+
isAccurate = true; // On surface is always accurate
|
|
205
205
|
break;
|
|
206
206
|
}
|
|
207
207
|
if (isAccurate) wallContactAccurateSamples++;
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
//
|
|
211
|
+
// Update accumulated time per state
|
|
212
212
|
if (typeof StateMachine !== 'undefined') {
|
|
213
213
|
const state = StateMachine.getState();
|
|
214
214
|
if (state !== lastObservedState) {
|
|
215
|
-
//
|
|
215
|
+
// Accumulate time for previous state
|
|
216
216
|
if (lastObservedState) {
|
|
217
217
|
const duration = now - lastStateChangeTime;
|
|
218
218
|
stateTimeAccum[lastObservedState] = (stateTimeAccum[lastObservedState] || 0) + duration;
|
|
@@ -224,31 +224,31 @@ const Metrics = (() => {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
// ===================================
|
|
227
|
-
//
|
|
227
|
+
// Event Hooks (non-destructive)
|
|
228
228
|
// ===================================
|
|
229
229
|
|
|
230
230
|
/**
|
|
231
|
-
* StateMachine
|
|
232
|
-
*
|
|
231
|
+
* Monitor StateMachine state transitions
|
|
232
|
+
* Wraps existing onStateChange callback to also collect metrics
|
|
233
233
|
*/
|
|
234
234
|
function _hookStateChanges() {
|
|
235
235
|
if (typeof StateMachine === 'undefined') return;
|
|
236
236
|
|
|
237
|
-
//
|
|
237
|
+
// Preserve existing callback
|
|
238
238
|
const originalCallback = StateMachine._metricsOriginalCallback;
|
|
239
239
|
|
|
240
240
|
StateMachine.setOnStateChange((prevState, newState) => {
|
|
241
|
-
//
|
|
241
|
+
// Metrics collection: record state transition
|
|
242
242
|
const now = Date.now();
|
|
243
243
|
stateTransitions.push({ from: prevState, to: newState, timestamp: now });
|
|
244
244
|
|
|
245
|
-
//
|
|
245
|
+
// Remove records older than TRANSITION_WINDOW
|
|
246
246
|
while (stateTransitions.length > 0 &&
|
|
247
247
|
stateTransitions[0].timestamp < now - TRANSITION_WINDOW) {
|
|
248
248
|
stateTransitions.shift();
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
//
|
|
251
|
+
// Accumulate previous state time
|
|
252
252
|
if (prevState) {
|
|
253
253
|
const duration = now - lastStateChangeTime;
|
|
254
254
|
stateTimeAccum[prevState] = (stateTimeAccum[prevState] || 0) + duration;
|
|
@@ -256,20 +256,20 @@ const Metrics = (() => {
|
|
|
256
256
|
lastObservedState = newState;
|
|
257
257
|
lastStateChangeTime = now;
|
|
258
258
|
|
|
259
|
-
//
|
|
260
|
-
// (app.js
|
|
261
|
-
// Metrics.init()
|
|
262
|
-
//
|
|
259
|
+
// Execute existing app.js callback chain
|
|
260
|
+
// (Since app.js calls setOnStateChange first,
|
|
261
|
+
// Metrics.init() called afterward overwrites the existing callback.
|
|
262
|
+
// Therefore we reproduce the app.js callback logic here.)
|
|
263
263
|
_invokeOriginalStateChangeHandler(prevState, newState);
|
|
264
264
|
});
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
/**
|
|
268
|
-
*
|
|
269
|
-
* Metrics
|
|
268
|
+
* Reproduce the original state change callback logic set in app.js
|
|
269
|
+
* Since Metrics overwrites setOnStateChange, the original behavior must be preserved.
|
|
270
270
|
*/
|
|
271
271
|
function _invokeOriginalStateChangeHandler(prevState, newState) {
|
|
272
|
-
//
|
|
272
|
+
// Sleep 'z' particles
|
|
273
273
|
if (newState === 'sleeping') {
|
|
274
274
|
const pet = document.getElementById('pet-container');
|
|
275
275
|
if (pet) {
|
|
@@ -290,7 +290,7 @@ const Metrics = (() => {
|
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
//
|
|
293
|
+
// Report state change to AI
|
|
294
294
|
if (window.clawmate && window.clawmate.reportToAI) {
|
|
295
295
|
window.clawmate.reportToAI('state_change', {
|
|
296
296
|
from: prevState, to: newState,
|
|
@@ -299,8 +299,8 @@ const Metrics = (() => {
|
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
/**
|
|
302
|
-
*
|
|
303
|
-
*
|
|
302
|
+
* Monitor user interactions (click events)
|
|
303
|
+
* Measure response time from click to state change
|
|
304
304
|
*/
|
|
305
305
|
function _hookUserInteractions() {
|
|
306
306
|
const petContainer = document.getElementById('pet-container');
|
|
@@ -311,40 +311,40 @@ const Metrics = (() => {
|
|
|
311
311
|
userClickCount++;
|
|
312
312
|
});
|
|
313
313
|
|
|
314
|
-
//
|
|
315
|
-
// (
|
|
314
|
+
// Record response time when state change occurs after click
|
|
315
|
+
// (Check when new entry is added to stateTransitions)
|
|
316
316
|
const origPush = Array.prototype.push;
|
|
317
317
|
const responseTimes = interactionResponseTimes;
|
|
318
318
|
const clickTimeRef = { get: () => lastClickTime };
|
|
319
319
|
|
|
320
|
-
// MutationObserver
|
|
321
|
-
//
|
|
320
|
+
// Instead of MutationObserver, handled inside _hookStateChanges
|
|
321
|
+
// Calculate time from click at the moment of state transition
|
|
322
322
|
setInterval(() => {
|
|
323
323
|
if (lastClickTime > 0 && stateTransitions.length > 0) {
|
|
324
324
|
const lastTransition = stateTransitions[stateTransitions.length - 1];
|
|
325
325
|
if (lastTransition.timestamp > lastClickTime) {
|
|
326
326
|
const responseTime = lastTransition.timestamp - lastClickTime;
|
|
327
|
-
// 3
|
|
327
|
+
// Only responses within 3 seconds are valid (beyond that is an unrelated transition)
|
|
328
328
|
if (responseTime < 3000) {
|
|
329
329
|
interactionResponseTimes.push(responseTime);
|
|
330
330
|
if (interactionResponseTimes.length > 50) {
|
|
331
331
|
interactionResponseTimes.shift();
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
|
-
lastClickTime = 0; //
|
|
334
|
+
lastClickTime = 0; // Measurement complete, reset
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
337
|
}, 500);
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
/**
|
|
341
|
-
*
|
|
342
|
-
* Speech
|
|
341
|
+
* Monitor speech bubble events
|
|
342
|
+
* Detect Speech module's show() calls and count them
|
|
343
343
|
*/
|
|
344
344
|
function _hookSpeechEvents() {
|
|
345
345
|
if (typeof Speech === 'undefined') return;
|
|
346
346
|
|
|
347
|
-
// Speech.show
|
|
347
|
+
// Wrap Speech.show to count invocations
|
|
348
348
|
const originalShow = Speech.show;
|
|
349
349
|
if (typeof originalShow === 'function') {
|
|
350
350
|
Speech.show = function(...args) {
|
|
@@ -355,19 +355,19 @@ const Metrics = (() => {
|
|
|
355
355
|
}
|
|
356
356
|
|
|
357
357
|
// ===================================
|
|
358
|
-
//
|
|
358
|
+
// Metric Calculations
|
|
359
359
|
// ===================================
|
|
360
360
|
|
|
361
361
|
/**
|
|
362
|
-
*
|
|
363
|
-
*
|
|
362
|
+
* Calculate movement smoothness
|
|
363
|
+
* Smaller variance of consecutive position changes = smoother
|
|
364
364
|
*
|
|
365
|
-
* @returns {number} 0~1 (1
|
|
365
|
+
* @returns {number} 0~1 (1 is smoothest)
|
|
366
366
|
*/
|
|
367
367
|
function _calcMovementSmoothness() {
|
|
368
368
|
if (positionSamples.length < 3) return 1.0;
|
|
369
369
|
|
|
370
|
-
//
|
|
370
|
+
// Calculate variance of consecutive movement vector magnitude changes
|
|
371
371
|
const deltas = [];
|
|
372
372
|
for (let i = 1; i < positionSamples.length; i++) {
|
|
373
373
|
const dx = positionSamples[i].x - positionSamples[i - 1].x;
|
|
@@ -377,7 +377,7 @@ const Metrics = (() => {
|
|
|
377
377
|
|
|
378
378
|
if (deltas.length < 2) return 1.0;
|
|
379
379
|
|
|
380
|
-
//
|
|
380
|
+
// Variance of differences between consecutive deltas (change in acceleration)
|
|
381
381
|
const accelChanges = [];
|
|
382
382
|
for (let i = 1; i < deltas.length; i++) {
|
|
383
383
|
accelChanges.push(Math.abs(deltas[i] - deltas[i - 1]));
|
|
@@ -385,17 +385,17 @@ const Metrics = (() => {
|
|
|
385
385
|
|
|
386
386
|
const avgAccelChange = accelChanges.reduce((a, b) => a + b, 0) / accelChanges.length;
|
|
387
387
|
|
|
388
|
-
//
|
|
389
|
-
//
|
|
388
|
+
// Convert variance to 0~1 score (larger avgAccelChange = less smooth)
|
|
389
|
+
// Abrupt change of 10px+ = 0, 0 = 1
|
|
390
390
|
const smoothness = Math.max(0, Math.min(1, 1 - (avgAccelChange / 10)));
|
|
391
391
|
return Math.round(smoothness * 100) / 100;
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
/**
|
|
395
|
-
*
|
|
396
|
-
*
|
|
395
|
+
* Calculate frame transition consistency
|
|
396
|
+
* Consistency of animation frame intervals (FPS stability)
|
|
397
397
|
*
|
|
398
|
-
* @returns {number} 0~1 (1
|
|
398
|
+
* @returns {number} 0~1 (1 is most consistent)
|
|
399
399
|
*/
|
|
400
400
|
function _calcFrameConsistency() {
|
|
401
401
|
if (fpsHistory.length < 2) return 1.0;
|
|
@@ -403,19 +403,19 @@ const Metrics = (() => {
|
|
|
403
403
|
const avg = fpsHistory.reduce((a, b) => a + b, 0) / fpsHistory.length;
|
|
404
404
|
if (avg === 0) return 0;
|
|
405
405
|
|
|
406
|
-
// FPS
|
|
406
|
+
// FPS standard deviation / mean (coefficient of variation)
|
|
407
407
|
const variance = fpsHistory.reduce((sum, fps) => sum + Math.pow(fps - avg, 2), 0) / fpsHistory.length;
|
|
408
408
|
const stdDev = Math.sqrt(variance);
|
|
409
|
-
const cv = stdDev / avg; //
|
|
409
|
+
const cv = stdDev / avg; // Coefficient of variation
|
|
410
410
|
|
|
411
|
-
// cv
|
|
411
|
+
// cv of 0 = perfect consistency, 0.5+ = very unstable
|
|
412
412
|
const consistency = Math.max(0, Math.min(1, 1 - cv * 2));
|
|
413
413
|
return Math.round(consistency * 100) / 100;
|
|
414
414
|
}
|
|
415
415
|
|
|
416
416
|
/**
|
|
417
|
-
*
|
|
418
|
-
*
|
|
417
|
+
* Aggregate state transition counts
|
|
418
|
+
* Transition count per state within the last 60 seconds
|
|
419
419
|
*
|
|
420
420
|
* @returns {object} { idle: n, walking: n, ... }
|
|
421
421
|
*/
|
|
@@ -431,8 +431,8 @@ const Metrics = (() => {
|
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
/**
|
|
434
|
-
* idle
|
|
435
|
-
*
|
|
434
|
+
* Calculate idle ratio
|
|
435
|
+
* Ratio of time spent in idle state within the report period
|
|
436
436
|
*
|
|
437
437
|
* @returns {number} 0~1
|
|
438
438
|
*/
|
|
@@ -446,8 +446,8 @@ const Metrics = (() => {
|
|
|
446
446
|
}
|
|
447
447
|
|
|
448
448
|
/**
|
|
449
|
-
*
|
|
450
|
-
*
|
|
449
|
+
* Calculate exploration coverage
|
|
450
|
+
* Ratio of visited cells in the 8x8 grid
|
|
451
451
|
*
|
|
452
452
|
* @returns {number} 0~1
|
|
453
453
|
*/
|
|
@@ -458,7 +458,7 @@ const Metrics = (() => {
|
|
|
458
458
|
}
|
|
459
459
|
|
|
460
460
|
/**
|
|
461
|
-
*
|
|
461
|
+
* Calculate wall contact accuracy
|
|
462
462
|
*
|
|
463
463
|
* @returns {number} 0~1
|
|
464
464
|
*/
|
|
@@ -469,9 +469,9 @@ const Metrics = (() => {
|
|
|
469
469
|
}
|
|
470
470
|
|
|
471
471
|
/**
|
|
472
|
-
*
|
|
472
|
+
* Calculate average interaction response time
|
|
473
473
|
*
|
|
474
|
-
* @returns {number} ms (
|
|
474
|
+
* @returns {number} ms (0 if no response records)
|
|
475
475
|
*/
|
|
476
476
|
function _calcAvgInteractionResponse() {
|
|
477
477
|
if (interactionResponseTimes.length === 0) return 0;
|
|
@@ -480,11 +480,11 @@ const Metrics = (() => {
|
|
|
480
480
|
}
|
|
481
481
|
|
|
482
482
|
// ===================================
|
|
483
|
-
//
|
|
483
|
+
// Snapshot and Summary
|
|
484
484
|
// ===================================
|
|
485
485
|
|
|
486
486
|
/**
|
|
487
|
-
*
|
|
487
|
+
* Return current metric snapshot (real-time)
|
|
488
488
|
* @returns {object}
|
|
489
489
|
*/
|
|
490
490
|
function getSnapshot() {
|
|
@@ -504,21 +504,21 @@ const Metrics = (() => {
|
|
|
504
504
|
}
|
|
505
505
|
|
|
506
506
|
/**
|
|
507
|
-
* 30
|
|
508
|
-
* @returns {object}
|
|
507
|
+
* Generate 30-second period summary + reset counters
|
|
508
|
+
* @returns {object} Metric summary data
|
|
509
509
|
*/
|
|
510
510
|
function getSummary() {
|
|
511
511
|
const now = Date.now();
|
|
512
512
|
const period = now - periodStartTime;
|
|
513
513
|
|
|
514
|
-
//
|
|
514
|
+
// Also accumulate time for the last observed state
|
|
515
515
|
if (lastObservedState) {
|
|
516
516
|
const duration = now - lastStateChangeTime;
|
|
517
517
|
stateTimeAccum[lastObservedState] = (stateTimeAccum[lastObservedState] || 0) + duration;
|
|
518
518
|
lastStateChangeTime = now;
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
-
//
|
|
521
|
+
// Calculate average FPS
|
|
522
522
|
const avgFps = fpsHistory.length > 0
|
|
523
523
|
? Math.round((fpsHistory.reduce((a, b) => a + b, 0) / fpsHistory.length) * 10) / 10
|
|
524
524
|
: 60;
|
|
@@ -538,65 +538,65 @@ const Metrics = (() => {
|
|
|
538
538
|
period: period,
|
|
539
539
|
};
|
|
540
540
|
|
|
541
|
-
//
|
|
541
|
+
// Reset period counters (keep accumulated data, only reset period counters)
|
|
542
542
|
periodStartTime = now;
|
|
543
543
|
speechCount = 0;
|
|
544
544
|
userClickCount = 0;
|
|
545
545
|
stateTimeAccum = {};
|
|
546
546
|
fpsHistory = [];
|
|
547
547
|
interactionResponseTimes = [];
|
|
548
|
-
// visitedGrid
|
|
549
|
-
// positionSamples
|
|
548
|
+
// visitedGrid is not reset (accumulated exploration record)
|
|
549
|
+
// positionSamples automatically removes old entries
|
|
550
550
|
|
|
551
551
|
return summary;
|
|
552
552
|
}
|
|
553
553
|
|
|
554
554
|
// ===================================
|
|
555
|
-
//
|
|
555
|
+
// Reporting
|
|
556
556
|
// ===================================
|
|
557
557
|
|
|
558
558
|
/**
|
|
559
|
-
*
|
|
559
|
+
* Send metric summary to main process every 30 seconds
|
|
560
560
|
*/
|
|
561
561
|
function _reportSummary() {
|
|
562
562
|
const summary = getSummary();
|
|
563
563
|
|
|
564
|
-
// main process
|
|
564
|
+
// Send to main process (via preload bridge)
|
|
565
565
|
if (window.clawmate && typeof window.clawmate.reportMetrics === 'function') {
|
|
566
566
|
window.clawmate.reportMetrics(summary);
|
|
567
567
|
}
|
|
568
568
|
|
|
569
|
-
//
|
|
569
|
+
// Brief console output (for debugging)
|
|
570
570
|
console.log(
|
|
571
571
|
`[Metrics] FPS:${summary.fps} | ` +
|
|
572
|
-
|
|
572
|
+
`smooth:${summary.movementSmoothness} | ` +
|
|
573
573
|
`idle:${(summary.idleRatio * 100).toFixed(0)}% | ` +
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
574
|
+
`explore:${(summary.explorationCoverage * 100).toFixed(0)}% | ` +
|
|
575
|
+
`clicks:${summary.userClicks} | ` +
|
|
576
|
+
`speech:${summary.speechCount}`
|
|
577
577
|
);
|
|
578
578
|
}
|
|
579
579
|
|
|
580
580
|
/**
|
|
581
|
-
*
|
|
582
|
-
*
|
|
581
|
+
* Reset exploration coverage grid
|
|
582
|
+
* Used when starting a new exploration session externally
|
|
583
583
|
*/
|
|
584
584
|
function resetExplorationGrid() {
|
|
585
585
|
visitedGrid.clear();
|
|
586
586
|
}
|
|
587
587
|
|
|
588
588
|
/**
|
|
589
|
-
*
|
|
589
|
+
* System cleanup
|
|
590
590
|
*/
|
|
591
591
|
function destroy() {
|
|
592
592
|
if (reportTimer) clearInterval(reportTimer);
|
|
593
593
|
if (sampleTimer) clearInterval(sampleTimer);
|
|
594
594
|
if (fpsRafId) cancelAnimationFrame(fpsRafId);
|
|
595
595
|
initialized = false;
|
|
596
|
-
console.log('[Metrics]
|
|
596
|
+
console.log('[Metrics] Self-observation system terminated');
|
|
597
597
|
}
|
|
598
598
|
|
|
599
|
-
// ---
|
|
599
|
+
// --- Public API ---
|
|
600
600
|
return {
|
|
601
601
|
init,
|
|
602
602
|
getSnapshot,
|