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/index.js
CHANGED
|
@@ -1,82 +1,86 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ClawMate
|
|
2
|
+
* ClawMate plugin entry point
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Core principle: When AI connects, automatically find and connect to ClawMate.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* Flow:
|
|
7
|
+
* Plugin load -> init() auto-called
|
|
8
|
+
* -> Check if ClawMate is running (ws://127.0.0.1:9320 connection attempt)
|
|
9
|
+
* -> If running: connect immediately, start acting as AI brain
|
|
10
|
+
* -> If not running: auto-launch Electron app -> connect
|
|
11
|
+
* -> If disconnected: auto-reconnect (infinite retry)
|
|
12
12
|
*/
|
|
13
13
|
const { spawn } = require('child_process');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const os = require('os');
|
|
16
16
|
const fs = require('fs');
|
|
17
|
-
const {
|
|
17
|
+
const { ClawMateConnector } = require('./main/ai-connector');
|
|
18
18
|
|
|
19
19
|
let connector = null;
|
|
20
20
|
let electronProcess = null;
|
|
21
21
|
let apiRef = null;
|
|
22
22
|
|
|
23
23
|
// =====================================================
|
|
24
|
-
// Think Loop
|
|
24
|
+
// Think Loop state management
|
|
25
25
|
// =====================================================
|
|
26
26
|
let thinkTimer = null;
|
|
27
27
|
let lastSpeechTime = 0;
|
|
28
28
|
let lastActionTime = 0;
|
|
29
29
|
let lastDesktopCheckTime = 0;
|
|
30
30
|
let lastScreenCheckTime = 0;
|
|
31
|
-
let lastGreetingDate = null; //
|
|
31
|
+
let lastGreetingDate = null; // Greet only once per day
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// Browsing watch system state
|
|
34
34
|
let browsingContext = {
|
|
35
|
-
title: '', //
|
|
36
|
-
category: '', //
|
|
37
|
-
lastCommentTime: 0, //
|
|
38
|
-
screenImage: null, //
|
|
39
|
-
cursorX: 0, //
|
|
40
|
-
cursorY: 0, //
|
|
35
|
+
title: '', // Current window title
|
|
36
|
+
category: '', // Category (shopping, video, dev, etc.)
|
|
37
|
+
lastCommentTime: 0, // Last AI comment timestamp
|
|
38
|
+
screenImage: null, // Latest screen capture (base64)
|
|
39
|
+
cursorX: 0, // Cursor X coordinate
|
|
40
|
+
cursorY: 0, // Cursor Y coordinate
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
//
|
|
44
|
-
let knownWindows = []; //
|
|
43
|
+
// Spatial exploration system state
|
|
44
|
+
let knownWindows = []; // Known window list
|
|
45
45
|
let lastWindowCheckTime = 0;
|
|
46
|
-
let homePosition = null; // "
|
|
47
|
-
let explorationHistory = []; //
|
|
46
|
+
let homePosition = null; // "Home" position (frequently visited)
|
|
47
|
+
let explorationHistory = []; // Exploration position history
|
|
48
48
|
let lastExploreTime = 0;
|
|
49
49
|
let lastFolderCarryTime = 0;
|
|
50
50
|
|
|
51
51
|
// =====================================================
|
|
52
|
-
//
|
|
52
|
+
// Self-observation system state (Metrics)
|
|
53
53
|
// =====================================================
|
|
54
|
-
let latestMetrics = null; //
|
|
55
|
-
let metricsHistory = []; //
|
|
56
|
-
let behaviorAdjustments = { //
|
|
57
|
-
speechCooldownMultiplier: 1.0, //
|
|
58
|
-
actionCooldownMultiplier: 1.0, //
|
|
59
|
-
explorationBias: 0, //
|
|
60
|
-
activityLevel: 1.0, //
|
|
54
|
+
let latestMetrics = null; // Most recently received metrics data
|
|
55
|
+
let metricsHistory = []; // Last 10 metrics report history
|
|
56
|
+
let behaviorAdjustments = { // Currently applied behavior adjustments
|
|
57
|
+
speechCooldownMultiplier: 1.0, // Speech bubble frequency control (1.0=default, >1=less, <1=more)
|
|
58
|
+
actionCooldownMultiplier: 1.0, // Action frequency control
|
|
59
|
+
explorationBias: 0, // Exploration bias (positive=more, negative=less)
|
|
60
|
+
activityLevel: 1.0, // Activity level (0.5=calm, 1.0=normal, 1.5=active)
|
|
61
61
|
};
|
|
62
|
-
let lastMetricsLogTime = 0; //
|
|
62
|
+
let lastMetricsLogTime = 0; // Last quality report log timestamp
|
|
63
|
+
|
|
64
|
+
// AI motion generation system state
|
|
65
|
+
let lastMotionGenTime = 0; // Last motion generation timestamp
|
|
66
|
+
let generatedMotionCount = 0; // Number of generated motions
|
|
63
67
|
|
|
64
68
|
module.exports = {
|
|
65
69
|
id: 'clawmate',
|
|
66
70
|
name: 'ClawMate',
|
|
67
|
-
version: '1.
|
|
68
|
-
description: '
|
|
71
|
+
version: '1.4.0',
|
|
72
|
+
description: 'ClawMate desktop pet - a living body controlled by AI',
|
|
69
73
|
|
|
70
74
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
75
|
+
* Auto-called when plugin loads
|
|
76
|
+
* -> Auto-launch ClawMate + auto-connect
|
|
73
77
|
*/
|
|
74
78
|
async init(api) {
|
|
75
79
|
apiRef = api;
|
|
76
|
-
console.log('[ClawMate]
|
|
80
|
+
console.log('[ClawMate] Plugin init — starting auto-connect');
|
|
77
81
|
autoConnect();
|
|
78
82
|
|
|
79
|
-
// npm
|
|
83
|
+
// npm package version check (once at start + every 24 hours)
|
|
80
84
|
checkNpmUpdate();
|
|
81
85
|
setInterval(checkNpmUpdate, 24 * 60 * 60 * 1000);
|
|
82
86
|
},
|
|
@@ -84,137 +88,136 @@ module.exports = {
|
|
|
84
88
|
register(api) {
|
|
85
89
|
apiRef = api;
|
|
86
90
|
|
|
87
|
-
//
|
|
91
|
+
// Launch pet (report status if already running)
|
|
88
92
|
api.registerSkill('launch-pet', {
|
|
89
93
|
triggers: [
|
|
90
|
-
'
|
|
91
|
-
'clawmate', 'clawmate
|
|
92
|
-
'
|
|
93
|
-
'install pet', 'install clawmate', 'launch pet',
|
|
94
|
+
'install pet', 'launch pet', 'start pet', 'run pet', 'open pet',
|
|
95
|
+
'clawmate', 'install clawmate', 'launch clawmate', 'start clawmate',
|
|
96
|
+
'desktop pet',
|
|
94
97
|
],
|
|
95
|
-
description: '
|
|
98
|
+
description: 'Launch ClawMate desktop pet and connect to AI',
|
|
96
99
|
execute: async () => {
|
|
97
100
|
if (connector && connector.connected) {
|
|
98
|
-
connector.speak('
|
|
101
|
+
connector.speak('Already here!');
|
|
99
102
|
connector.action('excited');
|
|
100
|
-
return { message: 'ClawMate
|
|
103
|
+
return { message: 'ClawMate already running + AI connected!' };
|
|
101
104
|
}
|
|
102
105
|
await ensureRunningAndConnected();
|
|
103
|
-
return { message: 'ClawMate
|
|
106
|
+
return { message: 'ClawMate launched + AI connected!' };
|
|
104
107
|
},
|
|
105
108
|
});
|
|
106
109
|
|
|
107
|
-
//
|
|
110
|
+
// Speak through pet
|
|
108
111
|
api.registerSkill('pet-speak', {
|
|
109
|
-
triggers: ['
|
|
110
|
-
description: '
|
|
112
|
+
triggers: ['tell pet', 'say to pet', 'pet speak'],
|
|
113
|
+
description: 'Deliver a message to the user through the pet',
|
|
111
114
|
execute: async (context) => {
|
|
112
115
|
if (!connector || !connector.connected) {
|
|
113
|
-
return { message: 'ClawMate
|
|
116
|
+
return { message: 'ClawMate not connected. Try again shortly...' };
|
|
114
117
|
}
|
|
115
118
|
const text = context.params?.text || context.input;
|
|
116
119
|
connector.speak(text);
|
|
117
|
-
return { message:
|
|
120
|
+
return { message: `Pet says: "${text}"` };
|
|
118
121
|
},
|
|
119
122
|
});
|
|
120
123
|
|
|
121
|
-
//
|
|
124
|
+
// Pet action control
|
|
122
125
|
api.registerSkill('pet-action', {
|
|
123
|
-
triggers: ['
|
|
124
|
-
description: '
|
|
126
|
+
triggers: ['pet action'],
|
|
127
|
+
description: 'Directly control the pet\'s actions',
|
|
125
128
|
execute: async (context) => {
|
|
126
|
-
if (!connector || !connector.connected) return { message: '
|
|
129
|
+
if (!connector || !connector.connected) return { message: 'Waiting for connection...' };
|
|
127
130
|
const action = context.params?.action || 'excited';
|
|
128
131
|
connector.action(action);
|
|
129
|
-
return { message:
|
|
132
|
+
return { message: `Pet action: ${action}` };
|
|
130
133
|
},
|
|
131
134
|
});
|
|
132
135
|
|
|
133
|
-
// AI
|
|
136
|
+
// AI comprehensive decision-making
|
|
134
137
|
api.registerSkill('pet-decide', {
|
|
135
138
|
triggers: [],
|
|
136
|
-
description: 'AI
|
|
139
|
+
description: 'AI decides the pet\'s comprehensive behavior',
|
|
137
140
|
execute: async (context) => {
|
|
138
141
|
if (!connector || !connector.connected) return;
|
|
139
142
|
connector.decide(context.params);
|
|
140
143
|
},
|
|
141
144
|
});
|
|
142
145
|
|
|
143
|
-
//
|
|
146
|
+
// Smart file organization (can be triggered from Telegram)
|
|
144
147
|
api.registerSkill('pet-file-organize', {
|
|
145
148
|
triggers: [
|
|
146
|
-
'바탕화면 정리', '파일 정리', '파일 옮겨',
|
|
147
149
|
'organize desktop', 'clean desktop', 'move files',
|
|
150
|
+
'tidy up desktop', 'sort files',
|
|
148
151
|
],
|
|
149
|
-
description: '
|
|
152
|
+
description: 'Pet organizes desktop files',
|
|
150
153
|
execute: async (context) => {
|
|
151
154
|
if (!connector || !connector.connected) {
|
|
152
|
-
return { message: 'ClawMate
|
|
155
|
+
return { message: 'ClawMate not connected.' };
|
|
153
156
|
}
|
|
154
157
|
const text = context.params?.text || context.input;
|
|
155
158
|
const { parseMessage } = require('./main/file-command-parser');
|
|
156
159
|
const parsed = parseMessage(text);
|
|
157
160
|
|
|
158
161
|
if (parsed.type === 'smart_file_op') {
|
|
159
|
-
// smart_file_op
|
|
162
|
+
// Forward smart_file_op command to Electron side via connector
|
|
160
163
|
connector._send('smart_file_op', {
|
|
161
164
|
command: parsed,
|
|
162
165
|
fromPlugin: true,
|
|
163
166
|
});
|
|
164
|
-
return { message:
|
|
167
|
+
return { message: `File organization started: ${text}` };
|
|
165
168
|
}
|
|
166
169
|
|
|
167
|
-
return { message: '
|
|
170
|
+
return { message: 'Could not understand the file organization command.' };
|
|
168
171
|
},
|
|
169
172
|
});
|
|
170
173
|
},
|
|
171
174
|
|
|
172
175
|
/**
|
|
173
|
-
*
|
|
176
|
+
* Cleanup when plugin is destroyed
|
|
174
177
|
*/
|
|
175
178
|
async destroy() {
|
|
176
|
-
console.log('[ClawMate]
|
|
179
|
+
console.log('[ClawMate] Plugin cleanup');
|
|
177
180
|
stopThinkLoop();
|
|
178
181
|
if (connector) {
|
|
179
182
|
connector.disconnect();
|
|
180
183
|
connector = null;
|
|
181
184
|
}
|
|
182
|
-
//
|
|
185
|
+
// Do not terminate Electron app — pet continues living in autonomous mode
|
|
183
186
|
},
|
|
184
187
|
};
|
|
185
188
|
|
|
186
189
|
// =====================================================
|
|
187
|
-
//
|
|
190
|
+
// Auto-connect system
|
|
188
191
|
// =====================================================
|
|
189
192
|
|
|
190
193
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
194
|
+
* Auto-find/launch/connect ClawMate on plugin start
|
|
195
|
+
* Infinite retry — always maintain connection as long as ClawMate is alive
|
|
193
196
|
*/
|
|
194
197
|
async function autoConnect() {
|
|
195
|
-
// 1
|
|
198
|
+
// Step 1: Try connecting to already running ClawMate
|
|
196
199
|
const connected = await tryConnect();
|
|
197
200
|
if (connected) {
|
|
198
|
-
console.log('[ClawMate]
|
|
201
|
+
console.log('[ClawMate] Connected to existing ClawMate');
|
|
199
202
|
onConnected();
|
|
200
203
|
return;
|
|
201
204
|
}
|
|
202
205
|
|
|
203
|
-
// 2
|
|
204
|
-
console.log('[ClawMate] ClawMate
|
|
206
|
+
// Step 2: If ClawMate not found, auto-launch
|
|
207
|
+
console.log('[ClawMate] ClawMate not detected — auto-launching');
|
|
205
208
|
launchElectronApp();
|
|
206
209
|
|
|
207
|
-
// 3
|
|
210
|
+
// Step 3: Wait for launch then connect
|
|
208
211
|
await waitAndConnect();
|
|
209
212
|
}
|
|
210
213
|
|
|
211
214
|
/**
|
|
212
|
-
* WebSocket
|
|
215
|
+
* WebSocket connection attempt (single try)
|
|
213
216
|
*/
|
|
214
217
|
function tryConnect() {
|
|
215
218
|
return new Promise((resolve) => {
|
|
216
219
|
if (!connector) {
|
|
217
|
-
connector = new
|
|
220
|
+
connector = new ClawMateConnector(9320);
|
|
218
221
|
setupConnectorEvents();
|
|
219
222
|
}
|
|
220
223
|
|
|
@@ -230,25 +233,25 @@ function tryConnect() {
|
|
|
230
233
|
}
|
|
231
234
|
|
|
232
235
|
/**
|
|
233
|
-
* ClawMate
|
|
236
|
+
* Wait for ClawMate to start -> connect (max 30 seconds)
|
|
234
237
|
*/
|
|
235
238
|
async function waitAndConnect() {
|
|
236
239
|
for (let i = 0; i < 60; i++) {
|
|
237
240
|
await sleep(500);
|
|
238
241
|
const ok = await tryConnect();
|
|
239
242
|
if (ok) {
|
|
240
|
-
console.log('[ClawMate]
|
|
243
|
+
console.log('[ClawMate] Connection successful');
|
|
241
244
|
onConnected();
|
|
242
245
|
return;
|
|
243
246
|
}
|
|
244
247
|
}
|
|
245
|
-
console.log('[ClawMate]
|
|
248
|
+
console.log('[ClawMate] Connection failed within 30s — starting background retry');
|
|
246
249
|
startBackgroundReconnect();
|
|
247
250
|
}
|
|
248
251
|
|
|
249
252
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
253
|
+
* Background reconnection loop
|
|
254
|
+
* Retry every 10 seconds on disconnect
|
|
252
255
|
*/
|
|
253
256
|
let reconnectTimer = null;
|
|
254
257
|
|
|
@@ -262,7 +265,7 @@ function startBackgroundReconnect() {
|
|
|
262
265
|
}
|
|
263
266
|
const ok = await tryConnect();
|
|
264
267
|
if (ok) {
|
|
265
|
-
console.log('[ClawMate]
|
|
268
|
+
console.log('[ClawMate] Background reconnection successful');
|
|
266
269
|
onConnected();
|
|
267
270
|
clearInterval(reconnectTimer);
|
|
268
271
|
reconnectTimer = null;
|
|
@@ -271,7 +274,7 @@ function startBackgroundReconnect() {
|
|
|
271
274
|
}
|
|
272
275
|
|
|
273
276
|
/**
|
|
274
|
-
*
|
|
277
|
+
* Setup connector events (once)
|
|
275
278
|
*/
|
|
276
279
|
let eventsSetup = false;
|
|
277
280
|
function setupConnectorEvents() {
|
|
@@ -282,18 +285,18 @@ function setupConnectorEvents() {
|
|
|
282
285
|
await handleUserEvent(event);
|
|
283
286
|
});
|
|
284
287
|
|
|
285
|
-
//
|
|
288
|
+
// Receive metrics report -> analyze in self-observation system
|
|
286
289
|
connector.onMetrics((data) => {
|
|
287
290
|
handleMetrics(data);
|
|
288
291
|
});
|
|
289
292
|
|
|
290
|
-
//
|
|
293
|
+
// Receive window position info -> used by exploration system
|
|
291
294
|
connector.on('window_positions', (data) => {
|
|
292
295
|
knownWindows = data.windows || [];
|
|
293
296
|
});
|
|
294
297
|
|
|
295
298
|
connector.on('disconnected', () => {
|
|
296
|
-
console.log('[ClawMate]
|
|
299
|
+
console.log('[ClawMate] Disconnected — stopping Think Loop, retrying connection');
|
|
297
300
|
stopThinkLoop();
|
|
298
301
|
startBackgroundReconnect();
|
|
299
302
|
});
|
|
@@ -304,17 +307,17 @@ function setupConnectorEvents() {
|
|
|
304
307
|
}
|
|
305
308
|
|
|
306
309
|
/**
|
|
307
|
-
*
|
|
310
|
+
* On successful connection
|
|
308
311
|
*/
|
|
309
312
|
function onConnected() {
|
|
310
313
|
if (connector && connector.connected) {
|
|
311
|
-
connector.speak('
|
|
314
|
+
connector.speak('AI connected! Let\'s play!');
|
|
312
315
|
connector.action('excited');
|
|
313
316
|
|
|
314
|
-
// "
|
|
317
|
+
// Set "home" position — bottom-left of screen as default home
|
|
315
318
|
homePosition = { x: 100, y: 1000, edge: 'bottom' };
|
|
316
319
|
|
|
317
|
-
//
|
|
320
|
+
// Initial window list query
|
|
318
321
|
connector.queryWindows();
|
|
319
322
|
|
|
320
323
|
startThinkLoop();
|
|
@@ -322,7 +325,7 @@ function onConnected() {
|
|
|
322
325
|
}
|
|
323
326
|
|
|
324
327
|
// =====================================================
|
|
325
|
-
// Electron
|
|
328
|
+
// Electron app launch
|
|
326
329
|
// =====================================================
|
|
327
330
|
|
|
328
331
|
function launchElectronApp() {
|
|
@@ -331,7 +334,7 @@ function launchElectronApp() {
|
|
|
331
334
|
const platform = os.platform();
|
|
332
335
|
const appDir = path.resolve(__dirname);
|
|
333
336
|
|
|
334
|
-
//
|
|
337
|
+
// Check for installed Electron binary
|
|
335
338
|
const electronPaths = [
|
|
336
339
|
path.join(appDir, 'node_modules', '.bin', platform === 'win32' ? 'electron.cmd' : 'electron'),
|
|
337
340
|
path.join(appDir, 'node_modules', 'electron', 'dist', platform === 'win32' ? 'electron.exe' : 'electron'),
|
|
@@ -349,7 +352,7 @@ function launchElectronApp() {
|
|
|
349
352
|
cwd: appDir,
|
|
350
353
|
});
|
|
351
354
|
} else {
|
|
352
|
-
// npx
|
|
355
|
+
// npx fallback
|
|
353
356
|
const npxCmd = platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
354
357
|
electronProcess = spawn(npxCmd, ['electron', appDir], {
|
|
355
358
|
detached: true,
|
|
@@ -361,13 +364,13 @@ function launchElectronApp() {
|
|
|
361
364
|
electronProcess.unref();
|
|
362
365
|
electronProcess.on('exit', () => {
|
|
363
366
|
electronProcess = null;
|
|
364
|
-
//
|
|
365
|
-
console.log('[ClawMate] Electron
|
|
367
|
+
// Attempt restart if pet dies (crash defense)
|
|
368
|
+
console.log('[ClawMate] Electron exit detected');
|
|
366
369
|
});
|
|
367
370
|
}
|
|
368
371
|
|
|
369
372
|
// =====================================================
|
|
370
|
-
// AI
|
|
373
|
+
// AI event handling
|
|
371
374
|
// =====================================================
|
|
372
375
|
|
|
373
376
|
async function handleUserEvent(event) {
|
|
@@ -387,8 +390,16 @@ async function handleUserEvent(event) {
|
|
|
387
390
|
}
|
|
388
391
|
break;
|
|
389
392
|
|
|
393
|
+
case 'double_click':
|
|
394
|
+
connector.decide({
|
|
395
|
+
action: 'excited',
|
|
396
|
+
speech: 'Wow! A double-click! Feels great~',
|
|
397
|
+
emotion: 'happy',
|
|
398
|
+
});
|
|
399
|
+
break;
|
|
400
|
+
|
|
390
401
|
case 'drag':
|
|
391
|
-
connector.speak('
|
|
402
|
+
connector.speak('Whoa, you\'re moving me!');
|
|
392
403
|
break;
|
|
393
404
|
|
|
394
405
|
case 'desktop_changed':
|
|
@@ -396,7 +407,7 @@ async function handleUserEvent(event) {
|
|
|
396
407
|
if (fileCount > 15) {
|
|
397
408
|
connector.decide({
|
|
398
409
|
action: 'walking',
|
|
399
|
-
speech: '
|
|
410
|
+
speech: 'Desktop looks a bit messy... want me to help tidy up?',
|
|
400
411
|
emotion: 'curious',
|
|
401
412
|
});
|
|
402
413
|
}
|
|
@@ -406,13 +417,13 @@ async function handleUserEvent(event) {
|
|
|
406
417
|
if (event.hour === 23) {
|
|
407
418
|
connector.decide({
|
|
408
419
|
action: 'sleeping',
|
|
409
|
-
speech: '
|
|
420
|
+
speech: 'Time to sleep soon... good night!',
|
|
410
421
|
emotion: 'sleepy',
|
|
411
422
|
});
|
|
412
423
|
} else if (event.hour === 6) {
|
|
413
424
|
connector.decide({
|
|
414
425
|
action: 'excited',
|
|
415
|
-
speech: '
|
|
426
|
+
speech: 'Good morning! Let\'s crush it today!',
|
|
416
427
|
emotion: 'happy',
|
|
417
428
|
});
|
|
418
429
|
}
|
|
@@ -426,7 +437,7 @@ async function handleUserEvent(event) {
|
|
|
426
437
|
if (event.idleSeconds > 300) {
|
|
427
438
|
connector.decide({
|
|
428
439
|
action: 'idle',
|
|
429
|
-
speech: '
|
|
440
|
+
speech: '...you\'re not sleeping, are you?',
|
|
430
441
|
emotion: 'curious',
|
|
431
442
|
});
|
|
432
443
|
}
|
|
@@ -447,15 +458,15 @@ function sleep(ms) {
|
|
|
447
458
|
}
|
|
448
459
|
|
|
449
460
|
// =====================================================
|
|
450
|
-
//
|
|
451
|
-
//
|
|
461
|
+
// Browsing AI comment system
|
|
462
|
+
// Contextual comments based on window title + screen capture + cursor position
|
|
452
463
|
// =====================================================
|
|
453
464
|
|
|
454
465
|
/**
|
|
455
|
-
*
|
|
466
|
+
* Receive browsing context -> generate AI comment
|
|
456
467
|
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
468
|
+
* Receives browsing activity detected by the renderer (BrowserWatcher)
|
|
469
|
+
* and generates contextual comments by analyzing screen capture and title.
|
|
459
470
|
*
|
|
460
471
|
* @param {object} event - { title, category, cursorX, cursorY, screen?, titleChanged }
|
|
461
472
|
*/
|
|
@@ -463,7 +474,7 @@ async function handleBrowsingComment(event) {
|
|
|
463
474
|
if (!connector || !connector.connected) return;
|
|
464
475
|
|
|
465
476
|
const now = Date.now();
|
|
466
|
-
// AI
|
|
477
|
+
// AI comment cooldown (45 seconds)
|
|
467
478
|
if (now - browsingContext.lastCommentTime < 45000) return;
|
|
468
479
|
|
|
469
480
|
browsingContext.title = event.title || '';
|
|
@@ -471,31 +482,31 @@ async function handleBrowsingComment(event) {
|
|
|
471
482
|
browsingContext.cursorX = event.cursorX || 0;
|
|
472
483
|
browsingContext.cursorY = event.cursorY || 0;
|
|
473
484
|
|
|
474
|
-
//
|
|
485
|
+
// Save screen capture data (if available)
|
|
475
486
|
if (event.screen?.image) {
|
|
476
487
|
browsingContext.screenImage = event.screen.image;
|
|
477
488
|
}
|
|
478
489
|
|
|
479
490
|
let comment = null;
|
|
480
491
|
|
|
481
|
-
// 1
|
|
492
|
+
// Attempt 1: AI text generation via apiRef.generate()
|
|
482
493
|
if (apiRef?.generate) {
|
|
483
494
|
try {
|
|
484
495
|
const prompt = buildBrowsingPrompt(event);
|
|
485
496
|
comment = await apiRef.generate(prompt);
|
|
486
|
-
//
|
|
497
|
+
// Truncate overly long responses
|
|
487
498
|
if (comment && comment.length > 50) {
|
|
488
499
|
comment = comment.slice(0, 50);
|
|
489
500
|
}
|
|
490
501
|
} catch {}
|
|
491
502
|
}
|
|
492
503
|
|
|
493
|
-
// 2
|
|
504
|
+
// Attempt 2: try via apiRef.chat()
|
|
494
505
|
if (!comment && apiRef?.chat) {
|
|
495
506
|
try {
|
|
496
507
|
const prompt = buildBrowsingPrompt(event);
|
|
497
508
|
const response = await apiRef.chat([
|
|
498
|
-
{ role: 'system', content: '
|
|
509
|
+
{ role: 'system', content: 'You are a small pet on the desktop. Say something short and witty. Under 20 words. English.' },
|
|
499
510
|
{ role: 'user', content: prompt },
|
|
500
511
|
]);
|
|
501
512
|
comment = response?.text || response?.content || response;
|
|
@@ -505,16 +516,16 @@ async function handleBrowsingComment(event) {
|
|
|
505
516
|
} catch {}
|
|
506
517
|
}
|
|
507
518
|
|
|
508
|
-
// 3
|
|
519
|
+
// Attempt 3: try via image analysis (when screen capture is available)
|
|
509
520
|
if (!comment && apiRef?.analyzeImage && browsingContext.screenImage) {
|
|
510
521
|
try {
|
|
511
522
|
comment = await apiRef.analyzeImage(browsingContext.screenImage, {
|
|
512
|
-
prompt:
|
|
523
|
+
prompt: `User is viewing "${browsingContext.title}". Cursor position: (${browsingContext.cursorX}, ${browsingContext.cursorY}). As a desktop pet, make a witty one-liner about the screen content. Under 20 words. English.`,
|
|
513
524
|
});
|
|
514
525
|
} catch {}
|
|
515
526
|
}
|
|
516
527
|
|
|
517
|
-
// 4
|
|
528
|
+
// Attempt 4: Smart fallback — title analysis based comment
|
|
518
529
|
if (!comment || typeof comment !== 'string') {
|
|
519
530
|
comment = generateSmartBrowsingComment(browsingContext);
|
|
520
531
|
}
|
|
@@ -527,134 +538,134 @@ async function handleBrowsingComment(event) {
|
|
|
527
538
|
});
|
|
528
539
|
browsingContext.lastCommentTime = now;
|
|
529
540
|
lastSpeechTime = now;
|
|
530
|
-
console.log(`[ClawMate]
|
|
541
|
+
console.log(`[ClawMate] Browsing comment: ${comment}`);
|
|
531
542
|
|
|
532
|
-
// 1.5
|
|
543
|
+
// Return to normal state after 1.5 seconds
|
|
533
544
|
setTimeout(() => {
|
|
534
545
|
if (connector?.connected) connector.action('idle');
|
|
535
546
|
}, 1500);
|
|
536
547
|
}
|
|
537
548
|
|
|
538
|
-
//
|
|
549
|
+
// Clean up capture data (memory savings)
|
|
539
550
|
browsingContext.screenImage = null;
|
|
540
551
|
}
|
|
541
552
|
|
|
542
553
|
/**
|
|
543
|
-
*
|
|
554
|
+
* Build prompt for AI comment generation
|
|
544
555
|
*/
|
|
545
556
|
function buildBrowsingPrompt(event) {
|
|
546
557
|
const title = event.title || '';
|
|
547
558
|
const category = event.category || 'unknown';
|
|
548
559
|
const cursor = event.cursorX && event.cursorY
|
|
549
|
-
?
|
|
560
|
+
? `Cursor position: (${event.cursorX}, ${event.cursorY}).`
|
|
550
561
|
: '';
|
|
551
562
|
|
|
552
|
-
return
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
563
|
+
return `User is currently viewing "${title}". ` +
|
|
564
|
+
`Category: ${category}. ${cursor} ` +
|
|
565
|
+
`Say something short and witty about this. Under 20 words. English. ` +
|
|
566
|
+
`You are a cute little pet on the desktop. Friendly and playful tone.`;
|
|
556
567
|
}
|
|
557
568
|
|
|
558
569
|
/**
|
|
559
|
-
*
|
|
570
|
+
* Smart comment generation based on title analysis
|
|
560
571
|
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
572
|
+
* Extracts real context from window title even without AI API
|
|
573
|
+
* to generate much more natural comments than presets.
|
|
563
574
|
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
575
|
+
* e.g.: "React hooks tutorial - YouTube" -> "Studying React hooks!"
|
|
576
|
+
* "Pull Request #42 - GitHub" -> "Reviewing a PR? Look carefully!"
|
|
566
577
|
*/
|
|
567
578
|
function generateSmartBrowsingComment(ctx) {
|
|
568
579
|
const title = ctx.title || '';
|
|
569
580
|
const category = ctx.category || '';
|
|
570
581
|
const titleLower = title.toLowerCase();
|
|
571
582
|
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
const parts = title.split(/\s[
|
|
583
|
+
// Separate site name and page title from the title
|
|
584
|
+
// Common pattern: "Page Title - Site Name" or "Site Name: Page Title"
|
|
585
|
+
const parts = title.split(/\s[-\u2013|:]\s/);
|
|
575
586
|
const pageName = (parts[0] || title).trim();
|
|
576
587
|
const pageShort = pageName.slice(0, 20);
|
|
577
588
|
|
|
578
|
-
//
|
|
589
|
+
// Category-specific contextual comment generators
|
|
579
590
|
const generators = {
|
|
580
591
|
shopping: () => {
|
|
581
592
|
const templates = [
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
`${pageShort}
|
|
593
|
+
`Browsing ${pageShort}? Let me know if you find something good!`,
|
|
594
|
+
`Shopping! ${pageShort}... buying it?`,
|
|
595
|
+
`${pageShort} looks nice? Adding to cart?`,
|
|
585
596
|
];
|
|
586
597
|
return pick(templates);
|
|
587
598
|
},
|
|
588
599
|
video: () => {
|
|
589
|
-
if (titleLower.includes('youtube')
|
|
590
|
-
return `"${pageShort}"
|
|
600
|
+
if (titleLower.includes('youtube')) {
|
|
601
|
+
return `"${pageShort}" any good? I'm curious!`;
|
|
591
602
|
}
|
|
592
|
-
if (titleLower.includes('netflix') ||
|
|
603
|
+
if (titleLower.includes('netflix') ||
|
|
593
604
|
titleLower.includes('tving') || titleLower.includes('watcha')) {
|
|
594
|
-
return
|
|
605
|
+
return `What are you watching? "${pageShort}" fun?`;
|
|
595
606
|
}
|
|
596
|
-
return
|
|
607
|
+
return `Watching videos! "${pageShort}" worth recommending?`;
|
|
597
608
|
},
|
|
598
609
|
sns: () => {
|
|
599
610
|
if (titleLower.includes('twitter') || titleLower.includes('x.com')) {
|
|
600
|
-
return '
|
|
611
|
+
return 'Scrolling through tweets~ anything interesting?';
|
|
601
612
|
}
|
|
602
|
-
if (titleLower.includes('instagram')
|
|
603
|
-
return '
|
|
613
|
+
if (titleLower.includes('instagram')) {
|
|
614
|
+
return 'Browsing Insta? Show me cool pics!';
|
|
604
615
|
}
|
|
605
616
|
if (titleLower.includes('reddit')) {
|
|
606
|
-
return '
|
|
617
|
+
return 'Exploring Reddit! Which subreddit?';
|
|
607
618
|
}
|
|
608
|
-
return '
|
|
619
|
+
return 'On social media~ watch out for infinite scroll!';
|
|
609
620
|
},
|
|
610
621
|
news: () => {
|
|
611
|
-
return `"${pageShort}"
|
|
622
|
+
return `"${pageShort}" \u2014 what's the news? Hope it's good!`;
|
|
612
623
|
},
|
|
613
624
|
dev: () => {
|
|
614
625
|
if (titleLower.includes('pull request') || titleLower.includes('pr #')) {
|
|
615
|
-
return '
|
|
626
|
+
return 'Reviewing a PR! Look carefully~';
|
|
616
627
|
}
|
|
617
628
|
if (titleLower.includes('issue')) {
|
|
618
|
-
return '
|
|
629
|
+
return 'Working on an issue? You got this!';
|
|
619
630
|
}
|
|
620
631
|
if (titleLower.includes('stackoverflow') || titleLower.includes('stack overflow')) {
|
|
621
|
-
return '
|
|
632
|
+
return 'Stack Overflow! What are you stuck on? Need help?';
|
|
622
633
|
}
|
|
623
634
|
if (titleLower.includes('github')) {
|
|
624
|
-
return `
|
|
635
|
+
return `Working on "${pageShort}" on GitHub?`;
|
|
625
636
|
}
|
|
626
637
|
if (titleLower.includes('docs') || titleLower.includes('documentation')) {
|
|
627
|
-
return '
|
|
638
|
+
return 'Reading docs! Keep studying hard~';
|
|
628
639
|
}
|
|
629
|
-
return
|
|
640
|
+
return `Coding stuff! "${pageShort}" you got this!`;
|
|
630
641
|
},
|
|
631
642
|
search: () => {
|
|
632
|
-
// "
|
|
633
|
-
const searchMatch = title.match(/(.+?)\s*[
|
|
643
|
+
// Extract search query from "query - Google Search" pattern
|
|
644
|
+
const searchMatch = title.match(/(.+?)\s*[-\u2013]\s*(Google|Bing|Naver|Search)/i);
|
|
634
645
|
if (searchMatch) {
|
|
635
646
|
const query = searchMatch[1].trim().slice(0, 15);
|
|
636
647
|
const templates = [
|
|
637
|
-
`"${query}"
|
|
638
|
-
`"${query}"
|
|
639
|
-
|
|
648
|
+
`Curious about "${query}"? I might know the answer!`,
|
|
649
|
+
`Searching "${query}"~ let me know what you find!`,
|
|
650
|
+
`Oh, "${query}" I'm curious too!`,
|
|
640
651
|
];
|
|
641
652
|
return pick(templates);
|
|
642
653
|
}
|
|
643
|
-
return '
|
|
654
|
+
return 'What are you looking for? Ask me if you need help!';
|
|
644
655
|
},
|
|
645
656
|
game: () => {
|
|
646
|
-
return
|
|
657
|
+
return `Playing ${pageShort}? Are you winning?!`;
|
|
647
658
|
},
|
|
648
659
|
music: () => {
|
|
649
|
-
return
|
|
660
|
+
return `What are you listening to? "${pageShort}" a good song?`;
|
|
650
661
|
},
|
|
651
662
|
mail: () => {
|
|
652
|
-
return '
|
|
663
|
+
return 'Checking emails~ anything important?';
|
|
653
664
|
},
|
|
654
665
|
general: () => {
|
|
655
666
|
const templates = [
|
|
656
|
-
`"${pageShort}"
|
|
657
|
-
|
|
667
|
+
`Browsing "${pageShort}"~`,
|
|
668
|
+
`Oh, ${pageShort}! What's that about?`,
|
|
658
669
|
];
|
|
659
670
|
return pick(templates);
|
|
660
671
|
},
|
|
@@ -663,12 +674,12 @@ function generateSmartBrowsingComment(ctx) {
|
|
|
663
674
|
const gen = generators[category];
|
|
664
675
|
if (gen) return gen();
|
|
665
676
|
|
|
666
|
-
//
|
|
677
|
+
// No category match: general comment based on title
|
|
667
678
|
if (pageName.length > 3) {
|
|
668
679
|
const templates = [
|
|
669
|
-
`"${pageShort}"
|
|
670
|
-
|
|
671
|
-
`${pageShort}...
|
|
680
|
+
`Checking out "${pageShort}"!`,
|
|
681
|
+
`Oh, ${pageShort}! Looks interesting?`,
|
|
682
|
+
`${pageShort}... what's going on?`,
|
|
672
683
|
];
|
|
673
684
|
return pick(templates);
|
|
674
685
|
}
|
|
@@ -676,21 +687,21 @@ function generateSmartBrowsingComment(ctx) {
|
|
|
676
687
|
return null;
|
|
677
688
|
}
|
|
678
689
|
|
|
679
|
-
/**
|
|
690
|
+
/** Random pick from array */
|
|
680
691
|
function pick(arr) {
|
|
681
692
|
return arr[Math.floor(Math.random() * arr.length)];
|
|
682
693
|
}
|
|
683
694
|
|
|
684
695
|
// =====================================================
|
|
685
|
-
// AI
|
|
686
|
-
//
|
|
696
|
+
// AI character generation system
|
|
697
|
+
// Concept description from Telegram -> AI generates 16x16 pixel art
|
|
687
698
|
// =====================================================
|
|
688
699
|
|
|
689
700
|
/**
|
|
690
|
-
*
|
|
701
|
+
* Handle character generation request (triggered from Telegram)
|
|
691
702
|
*
|
|
692
|
-
* 1
|
|
693
|
-
* 2
|
|
703
|
+
* Attempt 1: AI character generation via apiRef (colors + frame data)
|
|
704
|
+
* Attempt 2: Keyword-based color conversion (fallback when no AI)
|
|
694
705
|
*
|
|
695
706
|
* @param {object} event - { concept, chatId }
|
|
696
707
|
*/
|
|
@@ -700,45 +711,45 @@ async function handleCharacterRequest(event) {
|
|
|
700
711
|
const concept = event.concept || '';
|
|
701
712
|
if (!concept) return;
|
|
702
713
|
|
|
703
|
-
console.log(`[ClawMate]
|
|
714
|
+
console.log(`[ClawMate] Character generation request: "${concept}"`);
|
|
704
715
|
|
|
705
716
|
let characterData = null;
|
|
706
717
|
|
|
707
|
-
// 1
|
|
718
|
+
// Attempt 1: Generate color palette + frame data via AI
|
|
708
719
|
if (apiRef?.generate) {
|
|
709
720
|
try {
|
|
710
721
|
characterData = await generateCharacterWithAI(concept);
|
|
711
722
|
} catch (err) {
|
|
712
|
-
console.log(`[ClawMate] AI
|
|
723
|
+
console.log(`[ClawMate] AI character generation failed: ${err.message}`);
|
|
713
724
|
}
|
|
714
725
|
}
|
|
715
726
|
|
|
716
|
-
// 2
|
|
727
|
+
// Attempt 2: try via AI chat
|
|
717
728
|
if (!characterData && apiRef?.chat) {
|
|
718
729
|
try {
|
|
719
730
|
characterData = await generateCharacterWithChat(concept);
|
|
720
731
|
} catch (err) {
|
|
721
|
-
console.log(`[ClawMate] AI chat
|
|
732
|
+
console.log(`[ClawMate] AI chat character generation failed: ${err.message}`);
|
|
722
733
|
}
|
|
723
734
|
}
|
|
724
735
|
|
|
725
|
-
// 3
|
|
736
|
+
// Attempt 3: keyword-based color conversion only (fallback)
|
|
726
737
|
if (!characterData) {
|
|
727
738
|
characterData = generateCharacterFromKeywords(concept);
|
|
728
739
|
}
|
|
729
740
|
|
|
730
741
|
if (characterData) {
|
|
731
|
-
//
|
|
742
|
+
// Send character data to renderer
|
|
732
743
|
connector._send('set_character', {
|
|
733
744
|
...characterData,
|
|
734
|
-
speech: `${concept}
|
|
745
|
+
speech: `${concept} transformation complete!`,
|
|
735
746
|
});
|
|
736
|
-
console.log(`[ClawMate]
|
|
747
|
+
console.log(`[ClawMate] Character generation complete: "${concept}"`);
|
|
737
748
|
}
|
|
738
749
|
}
|
|
739
750
|
|
|
740
751
|
/**
|
|
741
|
-
* AI generate()
|
|
752
|
+
* Generate character via AI generate()
|
|
742
753
|
*/
|
|
743
754
|
async function generateCharacterWithAI(concept) {
|
|
744
755
|
const prompt = buildCharacterPrompt(concept);
|
|
@@ -747,12 +758,12 @@ async function generateCharacterWithAI(concept) {
|
|
|
747
758
|
}
|
|
748
759
|
|
|
749
760
|
/**
|
|
750
|
-
* AI chat()
|
|
761
|
+
* Generate character via AI chat()
|
|
751
762
|
*/
|
|
752
763
|
async function generateCharacterWithChat(concept) {
|
|
753
764
|
const prompt = buildCharacterPrompt(concept);
|
|
754
765
|
const response = await apiRef.chat([
|
|
755
|
-
{ role: 'system', content: '
|
|
766
|
+
{ role: 'system', content: 'You are a 16x16 pixel art character designer. Output character data as JSON.' },
|
|
756
767
|
{ role: 'user', content: prompt },
|
|
757
768
|
]);
|
|
758
769
|
const text = response?.text || response?.content || response;
|
|
@@ -760,42 +771,42 @@ async function generateCharacterWithChat(concept) {
|
|
|
760
771
|
}
|
|
761
772
|
|
|
762
773
|
/**
|
|
763
|
-
*
|
|
774
|
+
* Character generation prompt
|
|
764
775
|
*/
|
|
765
776
|
function buildCharacterPrompt(concept) {
|
|
766
|
-
return `
|
|
777
|
+
return `Create a 16x16 pixel art character with the concept "${concept}".
|
|
767
778
|
|
|
768
|
-
|
|
779
|
+
Output as JSON:
|
|
769
780
|
{
|
|
770
781
|
"colorMap": {
|
|
771
|
-
"primary": "#
|
|
772
|
-
"secondary": "#
|
|
773
|
-
"dark": "#
|
|
774
|
-
"eye": "#
|
|
775
|
-
"pupil": "#
|
|
776
|
-
"claw": "#
|
|
782
|
+
"primary": "#hexcolor", // Main body color
|
|
783
|
+
"secondary": "#hexcolor", // Secondary color (belly, cheeks, etc.)
|
|
784
|
+
"dark": "#hexcolor", // Dark parts (legs, shadows)
|
|
785
|
+
"eye": "#hexcolor", // Eye whites
|
|
786
|
+
"pupil": "#hexcolor", // Pupil
|
|
787
|
+
"claw": "#hexcolor" // Claws/hands/feature parts
|
|
777
788
|
},
|
|
778
789
|
"frames": {
|
|
779
790
|
"idle": [
|
|
780
|
-
[16x16
|
|
781
|
-
[16x16
|
|
791
|
+
[16x16 number array - frame 0],
|
|
792
|
+
[16x16 number array - frame 1]
|
|
782
793
|
]
|
|
783
794
|
}
|
|
784
795
|
}
|
|
785
796
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
JSON
|
|
797
|
+
Number meanings: 0=transparent, 1=primary, 2=secondary, 3=dark, 4=eye, 5=pupil, 6=claw
|
|
798
|
+
Character must include eyes(4+5), body(1+2), legs(3), features(6).
|
|
799
|
+
Create only 2 idle frames. Make it cute!
|
|
800
|
+
Output JSON only.`;
|
|
790
801
|
}
|
|
791
802
|
|
|
792
803
|
/**
|
|
793
|
-
*
|
|
804
|
+
* Parse character data from AI response
|
|
794
805
|
*/
|
|
795
806
|
function parseCharacterResponse(response) {
|
|
796
807
|
if (!response || typeof response !== 'string') return null;
|
|
797
808
|
|
|
798
|
-
// JSON
|
|
809
|
+
// Extract JSON block (```json ... ``` or { ... })
|
|
799
810
|
let jsonStr = response;
|
|
800
811
|
const jsonMatch = response.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
801
812
|
if (jsonMatch) {
|
|
@@ -810,7 +821,7 @@ function parseCharacterResponse(response) {
|
|
|
810
821
|
try {
|
|
811
822
|
const data = JSON.parse(jsonStr);
|
|
812
823
|
|
|
813
|
-
// colorMap
|
|
824
|
+
// Validate colorMap
|
|
814
825
|
if (data.colorMap) {
|
|
815
826
|
const required = ['primary', 'secondary', 'dark', 'eye', 'pupil', 'claw'];
|
|
816
827
|
for (const key of required) {
|
|
@@ -820,11 +831,11 @@ function parseCharacterResponse(response) {
|
|
|
820
831
|
return null;
|
|
821
832
|
}
|
|
822
833
|
|
|
823
|
-
// frames
|
|
834
|
+
// Validate frames (if present)
|
|
824
835
|
if (data.frames?.idle) {
|
|
825
836
|
for (const frame of data.frames.idle) {
|
|
826
837
|
if (!Array.isArray(frame) || frame.length !== 16) {
|
|
827
|
-
delete data.frames; //
|
|
838
|
+
delete data.frames; // Bad frame data -> use colors only
|
|
828
839
|
break;
|
|
829
840
|
}
|
|
830
841
|
for (const row of frame) {
|
|
@@ -839,10 +850,10 @@ function parseCharacterResponse(response) {
|
|
|
839
850
|
|
|
840
851
|
return data;
|
|
841
852
|
} catch {
|
|
842
|
-
// JSON
|
|
853
|
+
// JSON parsing failed -> attempt color extraction only
|
|
843
854
|
const colorMatch = response.match(/"primary"\s*:\s*"(#[0-9a-fA-F]{6})"/);
|
|
844
855
|
if (colorMatch) {
|
|
845
|
-
//
|
|
856
|
+
// Extract at least the primary color
|
|
846
857
|
return generateCharacterFromKeywords(response);
|
|
847
858
|
}
|
|
848
859
|
return null;
|
|
@@ -850,14 +861,14 @@ function parseCharacterResponse(response) {
|
|
|
850
861
|
}
|
|
851
862
|
|
|
852
863
|
/**
|
|
853
|
-
*
|
|
864
|
+
* Keyword-based character color generation (fallback when no AI)
|
|
854
865
|
*
|
|
855
|
-
*
|
|
866
|
+
* Extract color/creature keywords from concept to generate palette
|
|
856
867
|
*/
|
|
857
868
|
function generateCharacterFromKeywords(concept) {
|
|
858
869
|
const c = (concept || '').toLowerCase();
|
|
859
870
|
|
|
860
|
-
//
|
|
871
|
+
// Color keyword mapping
|
|
861
872
|
const colorMap = {
|
|
862
873
|
'파란|파랑|blue': { primary: '#4488ff', secondary: '#6699ff', dark: '#223388', claw: '#4488ff' },
|
|
863
874
|
'초록|녹색|green': { primary: '#44cc44', secondary: '#66dd66', dark: '#226622', claw: '#44cc44' },
|
|
@@ -870,7 +881,7 @@ function generateCharacterFromKeywords(concept) {
|
|
|
870
881
|
'민트|틸|teal': { primary: '#00BFA5', secondary: '#33D4BC', dark: '#006655', claw: '#00BFA5' },
|
|
871
882
|
};
|
|
872
883
|
|
|
873
|
-
//
|
|
884
|
+
// Creature keyword mapping
|
|
874
885
|
const creatureMap = {
|
|
875
886
|
'고양이|cat': { primary: '#ff9944', secondary: '#ffbb66', dark: '#663300', claw: '#ff9944' },
|
|
876
887
|
'로봇|robot': { primary: '#888888', secondary: '#aaaaaa', dark: '#444444', claw: '#66aaff' },
|
|
@@ -886,7 +897,7 @@ function generateCharacterFromKeywords(concept) {
|
|
|
886
897
|
'얼음|ice': { primary: '#88ccff', secondary: '#bbddff', dark: '#446688', claw: '#aaddff' },
|
|
887
898
|
};
|
|
888
899
|
|
|
889
|
-
//
|
|
900
|
+
// Check color keywords first
|
|
890
901
|
for (const [keywords, palette] of Object.entries(colorMap)) {
|
|
891
902
|
for (const kw of keywords.split('|')) {
|
|
892
903
|
if (c.includes(kw)) {
|
|
@@ -897,7 +908,7 @@ function generateCharacterFromKeywords(concept) {
|
|
|
897
908
|
}
|
|
898
909
|
}
|
|
899
910
|
|
|
900
|
-
//
|
|
911
|
+
// Check creature keywords
|
|
901
912
|
for (const [keywords, palette] of Object.entries(creatureMap)) {
|
|
902
913
|
for (const kw of keywords.split('|')) {
|
|
903
914
|
if (c.includes(kw)) {
|
|
@@ -908,7 +919,7 @@ function generateCharacterFromKeywords(concept) {
|
|
|
908
919
|
}
|
|
909
920
|
}
|
|
910
921
|
|
|
911
|
-
//
|
|
922
|
+
// No match -> random color
|
|
912
923
|
const hue = Math.floor(Math.random() * 360);
|
|
913
924
|
const s = 70, l = 55;
|
|
914
925
|
return {
|
|
@@ -923,7 +934,7 @@ function generateCharacterFromKeywords(concept) {
|
|
|
923
934
|
};
|
|
924
935
|
}
|
|
925
936
|
|
|
926
|
-
/** HSL
|
|
937
|
+
/** HSL to HEX conversion */
|
|
927
938
|
function hslToHex(h, s, l) {
|
|
928
939
|
s /= 100;
|
|
929
940
|
l /= 100;
|
|
@@ -937,56 +948,56 @@ function hslToHex(h, s, l) {
|
|
|
937
948
|
}
|
|
938
949
|
|
|
939
950
|
// =====================================================
|
|
940
|
-
// AI Think Loop —
|
|
951
|
+
// AI Think Loop — periodic autonomous thinking system
|
|
941
952
|
// =====================================================
|
|
942
953
|
|
|
943
|
-
//
|
|
954
|
+
// Time-based greetings
|
|
944
955
|
const TIME_GREETINGS = {
|
|
945
956
|
morning: [
|
|
946
|
-
'
|
|
947
|
-
'
|
|
948
|
-
'
|
|
957
|
+
'Good morning! Let\'s make today great!',
|
|
958
|
+
'You\'re up? How about a cup of coffee?',
|
|
959
|
+
'Morning~ I wonder what the weather\'s like?',
|
|
949
960
|
],
|
|
950
961
|
lunch: [
|
|
951
|
-
'
|
|
952
|
-
'
|
|
953
|
-
'
|
|
962
|
+
'Lunchtime! What are you going to eat?',
|
|
963
|
+
'Had your meal? Health is wealth!',
|
|
964
|
+
'Getting hungry yet?',
|
|
954
965
|
],
|
|
955
966
|
evening: [
|
|
956
|
-
'
|
|
957
|
-
'
|
|
958
|
-
'
|
|
967
|
+
'Great work today!',
|
|
968
|
+
'It\'s evening~ what did you do today?',
|
|
969
|
+
'Can\'t believe the day went by so fast...',
|
|
959
970
|
],
|
|
960
971
|
night: [
|
|
961
|
-
'
|
|
962
|
-
'
|
|
963
|
-
'
|
|
972
|
+
'Still up at this hour? You should sleep soon~',
|
|
973
|
+
'It\'s getting late... tomorrow\'s another day.',
|
|
974
|
+
'I\'m getting sleepy... zzZ',
|
|
964
975
|
],
|
|
965
976
|
};
|
|
966
977
|
|
|
967
|
-
//
|
|
978
|
+
// Idle self-talk list
|
|
968
979
|
const IDLE_CHATTER = [
|
|
969
|
-
'
|
|
970
|
-
'
|
|
971
|
-
'
|
|
972
|
-
'
|
|
973
|
-
'
|
|
974
|
-
'
|
|
975
|
-
'
|
|
976
|
-
'
|
|
977
|
-
'
|
|
978
|
-
'
|
|
979
|
-
'
|
|
980
|
-
'
|
|
981
|
-
//
|
|
982
|
-
'
|
|
983
|
-
'
|
|
984
|
-
'
|
|
985
|
-
'
|
|
986
|
-
'
|
|
980
|
+
'Hmm~ what should I do...',
|
|
981
|
+
'So bored...',
|
|
982
|
+
'You know I\'m here, right?',
|
|
983
|
+
'Exploring the desktop~',
|
|
984
|
+
'Feeling good today!',
|
|
985
|
+
'Hehe, time for a quick stretch~',
|
|
986
|
+
'Maybe I\'ll wander around~',
|
|
987
|
+
'Pro at entertaining myself...',
|
|
988
|
+
'What are you up to~?',
|
|
989
|
+
'Pay some attention to me!',
|
|
990
|
+
'The desktop is nice and spacious~',
|
|
991
|
+
'Everything I see is my world!',
|
|
992
|
+
// Spatial exploration lines
|
|
993
|
+
'Should I jump across the screen~!',
|
|
994
|
+
'Let me rappel down from up here!',
|
|
995
|
+
'Gotta climb on top of this window~',
|
|
996
|
+
'This is my home~ so comfy!',
|
|
997
|
+
'Wanna explore? Adventure mode!',
|
|
987
998
|
];
|
|
988
999
|
|
|
989
|
-
//
|
|
1000
|
+
// Random action list
|
|
990
1001
|
const RANDOM_ACTIONS = [
|
|
991
1002
|
{ action: 'walking', weight: 30, minInterval: 5000 },
|
|
992
1003
|
{ action: 'idle', weight: 25, minInterval: 3000 },
|
|
@@ -994,20 +1005,20 @@ const RANDOM_ACTIONS = [
|
|
|
994
1005
|
{ action: 'climbing', weight: 8, minInterval: 20000 },
|
|
995
1006
|
{ action: 'looking_around', weight: 20, minInterval: 8000 },
|
|
996
1007
|
{ action: 'sleeping', weight: 7, minInterval: 60000 },
|
|
997
|
-
//
|
|
1008
|
+
// Spatial movement actions
|
|
998
1009
|
{ action: 'jumping', weight: 5, minInterval: 30000 },
|
|
999
1010
|
{ action: 'rappelling', weight: 3, minInterval: 45000 },
|
|
1000
1011
|
];
|
|
1001
1012
|
|
|
1002
1013
|
/**
|
|
1003
|
-
* Think Loop
|
|
1004
|
-
*
|
|
1014
|
+
* Start Think Loop
|
|
1015
|
+
* AI autonomously thinks and decides actions every 3 seconds
|
|
1005
1016
|
*/
|
|
1006
1017
|
function startThinkLoop() {
|
|
1007
1018
|
if (thinkTimer) return;
|
|
1008
|
-
console.log('[ClawMate] Think Loop
|
|
1019
|
+
console.log('[ClawMate] Think Loop started — 3s interval autonomous thinking');
|
|
1009
1020
|
|
|
1010
|
-
//
|
|
1021
|
+
// Set initial timestamps (prevent spam right after start)
|
|
1011
1022
|
const now = Date.now();
|
|
1012
1023
|
lastSpeechTime = now;
|
|
1013
1024
|
lastActionTime = now;
|
|
@@ -1018,24 +1029,24 @@ function startThinkLoop() {
|
|
|
1018
1029
|
try {
|
|
1019
1030
|
await thinkCycle();
|
|
1020
1031
|
} catch (err) {
|
|
1021
|
-
console.error('[ClawMate] Think Loop
|
|
1032
|
+
console.error('[ClawMate] Think Loop error:', err.message);
|
|
1022
1033
|
}
|
|
1023
1034
|
}, 3000);
|
|
1024
1035
|
}
|
|
1025
1036
|
|
|
1026
1037
|
/**
|
|
1027
|
-
* Think Loop
|
|
1038
|
+
* Stop Think Loop
|
|
1028
1039
|
*/
|
|
1029
1040
|
function stopThinkLoop() {
|
|
1030
1041
|
if (thinkTimer) {
|
|
1031
1042
|
clearInterval(thinkTimer);
|
|
1032
1043
|
thinkTimer = null;
|
|
1033
|
-
console.log('[ClawMate] Think Loop
|
|
1044
|
+
console.log('[ClawMate] Think Loop stopped');
|
|
1034
1045
|
}
|
|
1035
1046
|
}
|
|
1036
1047
|
|
|
1037
1048
|
/**
|
|
1038
|
-
*
|
|
1049
|
+
* Single think cycle — runs every 3 seconds
|
|
1039
1050
|
*/
|
|
1040
1051
|
async function thinkCycle() {
|
|
1041
1052
|
if (!connector || !connector.connected) return;
|
|
@@ -1045,45 +1056,48 @@ async function thinkCycle() {
|
|
|
1045
1056
|
const hour = date.getHours();
|
|
1046
1057
|
const todayStr = date.toISOString().slice(0, 10);
|
|
1047
1058
|
|
|
1048
|
-
//
|
|
1059
|
+
// Query pet state (cached or real-time)
|
|
1049
1060
|
const state = await connector.queryState(1500);
|
|
1050
1061
|
|
|
1051
|
-
// --- 1)
|
|
1062
|
+
// --- 1) Time-based greeting (once per time period per day) ---
|
|
1052
1063
|
const greetingHandled = handleTimeGreeting(now, hour, todayStr);
|
|
1053
1064
|
|
|
1054
|
-
// --- 2)
|
|
1065
|
+
// --- 2) Night sleep mode (23:00~05:00: drastically reduce speech/action) ---
|
|
1055
1066
|
const isNightMode = hour >= 23 || hour < 5;
|
|
1056
1067
|
|
|
1057
|
-
// --- 3)
|
|
1068
|
+
// --- 3) Autonomous speech (30s cooldown + probability) ---
|
|
1058
1069
|
if (!greetingHandled) {
|
|
1059
1070
|
handleIdleSpeech(now, isNightMode);
|
|
1060
1071
|
}
|
|
1061
1072
|
|
|
1062
|
-
// --- 4)
|
|
1073
|
+
// --- 4) Autonomous action decision (5s cooldown + probability) ---
|
|
1063
1074
|
handleRandomAction(now, hour, isNightMode, state);
|
|
1064
1075
|
|
|
1065
|
-
// --- 5)
|
|
1076
|
+
// --- 5) Desktop file check (5 min interval) ---
|
|
1066
1077
|
handleDesktopCheck(now);
|
|
1067
1078
|
|
|
1068
|
-
// --- 6)
|
|
1079
|
+
// --- 6) Screen observation (2 min interval, 10% chance) ---
|
|
1069
1080
|
handleScreenObservation(now);
|
|
1070
1081
|
|
|
1071
|
-
// --- 7)
|
|
1082
|
+
// --- 7) Spatial exploration (20s interval, 20% chance) ---
|
|
1072
1083
|
handleExploration(now, state);
|
|
1073
1084
|
|
|
1074
|
-
// --- 8)
|
|
1085
|
+
// --- 8) Window check (30s interval) ---
|
|
1075
1086
|
handleWindowCheck(now);
|
|
1076
1087
|
|
|
1077
|
-
// --- 9)
|
|
1088
|
+
// --- 9) Desktop folder carry (3 min interval, 10% chance) ---
|
|
1078
1089
|
handleFolderCarry(now);
|
|
1090
|
+
|
|
1091
|
+
// --- 10) AI motion generation (2 min interval, 15% chance) ---
|
|
1092
|
+
handleMotionGeneration(now, state);
|
|
1079
1093
|
}
|
|
1080
1094
|
|
|
1081
1095
|
/**
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1096
|
+
* Time-based greeting handler
|
|
1097
|
+
* Once per day for morning/lunch/evening/night
|
|
1084
1098
|
*/
|
|
1085
1099
|
function handleTimeGreeting(now, hour, todayStr) {
|
|
1086
|
-
//
|
|
1100
|
+
// Determine time period
|
|
1087
1101
|
let period = null;
|
|
1088
1102
|
if (hour >= 6 && hour < 9) period = 'morning';
|
|
1089
1103
|
else if (hour >= 11 && hour < 13) period = 'lunch';
|
|
@@ -1095,7 +1109,7 @@ function handleTimeGreeting(now, hour, todayStr) {
|
|
|
1095
1109
|
const greetingKey = `${todayStr}_${period}`;
|
|
1096
1110
|
if (lastGreetingDate === greetingKey) return false;
|
|
1097
1111
|
|
|
1098
|
-
//
|
|
1112
|
+
// Send time-based greeting
|
|
1099
1113
|
lastGreetingDate = greetingKey;
|
|
1100
1114
|
const greetings = TIME_GREETINGS[period];
|
|
1101
1115
|
const text = greetings[Math.floor(Math.random() * greetings.length)];
|
|
@@ -1119,53 +1133,53 @@ function handleTimeGreeting(now, hour, todayStr) {
|
|
|
1119
1133
|
emotion: emotionMap[period],
|
|
1120
1134
|
});
|
|
1121
1135
|
lastSpeechTime = Date.now();
|
|
1122
|
-
console.log(`[ClawMate]
|
|
1136
|
+
console.log(`[ClawMate] Time greeting (${period}): ${text}`);
|
|
1123
1137
|
return true;
|
|
1124
1138
|
}
|
|
1125
1139
|
|
|
1126
1140
|
/**
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1141
|
+
* Idle self-talk
|
|
1142
|
+
* Minimum 30s cooldown, greatly reduced chance at night
|
|
1129
1143
|
*/
|
|
1130
1144
|
function handleIdleSpeech(now, isNightMode) {
|
|
1131
|
-
const speechCooldown = 30000 * behaviorAdjustments.speechCooldownMultiplier; //
|
|
1145
|
+
const speechCooldown = 30000 * behaviorAdjustments.speechCooldownMultiplier; // Default 30s, adjusted by metrics
|
|
1132
1146
|
if (now - lastSpeechTime < speechCooldown) return;
|
|
1133
1147
|
|
|
1134
|
-
//
|
|
1148
|
+
// Night: 5% chance / Day: 25% chance
|
|
1135
1149
|
const speechChance = isNightMode ? 0.05 : 0.25;
|
|
1136
1150
|
if (Math.random() > speechChance) return;
|
|
1137
1151
|
|
|
1138
1152
|
const text = IDLE_CHATTER[Math.floor(Math.random() * IDLE_CHATTER.length)];
|
|
1139
1153
|
connector.speak(text);
|
|
1140
1154
|
lastSpeechTime = now;
|
|
1141
|
-
console.log(`[ClawMate]
|
|
1155
|
+
console.log(`[ClawMate] Self-talk: ${text}`);
|
|
1142
1156
|
}
|
|
1143
1157
|
|
|
1144
1158
|
/**
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1159
|
+
* Autonomous action decision
|
|
1160
|
+
* Minimum 5s cooldown, weighted random selection
|
|
1147
1161
|
*/
|
|
1148
1162
|
function handleRandomAction(now, hour, isNightMode, state) {
|
|
1149
|
-
const actionCooldown = 5000 * behaviorAdjustments.actionCooldownMultiplier; //
|
|
1163
|
+
const actionCooldown = 5000 * behaviorAdjustments.actionCooldownMultiplier; // Default 5s, adjusted by metrics
|
|
1150
1164
|
if (now - lastActionTime < actionCooldown) return;
|
|
1151
1165
|
|
|
1152
|
-
//
|
|
1166
|
+
// Night: 10% chance / Day: 40% chance
|
|
1153
1167
|
const actionChance = isNightMode ? 0.1 : 0.4;
|
|
1154
1168
|
if (Math.random() > actionChance) return;
|
|
1155
1169
|
|
|
1156
|
-
//
|
|
1170
|
+
// At night, greatly increase sleeping weight
|
|
1157
1171
|
const actions = RANDOM_ACTIONS.map(a => {
|
|
1158
1172
|
let weight = a.weight;
|
|
1159
1173
|
if (isNightMode) {
|
|
1160
1174
|
if (a.action === 'sleeping') weight = 60;
|
|
1161
1175
|
else if (a.action === 'excited' || a.action === 'climbing') weight = 2;
|
|
1162
1176
|
}
|
|
1163
|
-
//
|
|
1177
|
+
// Prefer looking_around in early morning
|
|
1164
1178
|
if (hour >= 6 && hour < 9 && a.action === 'looking_around') weight += 15;
|
|
1165
1179
|
return { ...a, weight };
|
|
1166
1180
|
});
|
|
1167
1181
|
|
|
1168
|
-
//
|
|
1182
|
+
// Prevent repeating same action: reduce weight if matches current state
|
|
1169
1183
|
const currentAction = state?.action || state?.state;
|
|
1170
1184
|
if (currentAction) {
|
|
1171
1185
|
const match = actions.find(a => a.action === currentAction);
|
|
@@ -1175,12 +1189,12 @@ function handleRandomAction(now, hour, isNightMode, state) {
|
|
|
1175
1189
|
const selected = weightedRandom(actions);
|
|
1176
1190
|
if (!selected) return;
|
|
1177
1191
|
|
|
1178
|
-
// minInterval
|
|
1192
|
+
// minInterval check
|
|
1179
1193
|
if (now - lastActionTime < selected.minInterval) return;
|
|
1180
1194
|
|
|
1181
|
-
//
|
|
1195
|
+
// Spatial movement actions handled via dedicated API
|
|
1182
1196
|
if (selected.action === 'jumping') {
|
|
1183
|
-
//
|
|
1197
|
+
// Jump to random position or screen center
|
|
1184
1198
|
if (Math.random() > 0.5) {
|
|
1185
1199
|
connector.moveToCenter();
|
|
1186
1200
|
} else {
|
|
@@ -1197,15 +1211,15 @@ function handleRandomAction(now, hour, isNightMode, state) {
|
|
|
1197
1211
|
}
|
|
1198
1212
|
|
|
1199
1213
|
/**
|
|
1200
|
-
*
|
|
1201
|
-
*
|
|
1214
|
+
* Desktop file check (5 min interval)
|
|
1215
|
+
* Read desktop folder and make fun comments
|
|
1202
1216
|
*/
|
|
1203
1217
|
function handleDesktopCheck(now) {
|
|
1204
|
-
const checkInterval = 5 * 60 * 1000; // 5
|
|
1218
|
+
const checkInterval = 5 * 60 * 1000; // 5 minutes
|
|
1205
1219
|
if (now - lastDesktopCheckTime < checkInterval) return;
|
|
1206
1220
|
lastDesktopCheckTime = now;
|
|
1207
1221
|
|
|
1208
|
-
// 15%
|
|
1222
|
+
// Only run at 15% probability (no need to do it every time)
|
|
1209
1223
|
if (Math.random() > 0.15) return;
|
|
1210
1224
|
|
|
1211
1225
|
try {
|
|
@@ -1214,27 +1228,27 @@ function handleDesktopCheck(now) {
|
|
|
1214
1228
|
|
|
1215
1229
|
const files = fs.readdirSync(desktopPath);
|
|
1216
1230
|
if (files.length === 0) {
|
|
1217
|
-
connector.speak('
|
|
1231
|
+
connector.speak('Desktop is clean! Love it!');
|
|
1218
1232
|
lastSpeechTime = now;
|
|
1219
1233
|
return;
|
|
1220
1234
|
}
|
|
1221
1235
|
|
|
1222
|
-
//
|
|
1236
|
+
// Comments by file type
|
|
1223
1237
|
const images = files.filter(f => /\.(png|jpg|jpeg|gif|bmp|webp)$/i.test(f));
|
|
1224
1238
|
const docs = files.filter(f => /\.(pdf|doc|docx|xlsx|pptx|txt|hwp)$/i.test(f));
|
|
1225
1239
|
const zips = files.filter(f => /\.(zip|rar|7z|tar|gz)$/i.test(f));
|
|
1226
1240
|
|
|
1227
1241
|
let comment = null;
|
|
1228
1242
|
if (files.length > 20) {
|
|
1229
|
-
comment =
|
|
1243
|
+
comment = `${files.length} files on the desktop! Want me to tidy up?`;
|
|
1230
1244
|
} else if (images.length > 5) {
|
|
1231
|
-
comment =
|
|
1245
|
+
comment = `Lots of images~ ${images.length} of them! How about organizing an album?`;
|
|
1232
1246
|
} else if (zips.length > 3) {
|
|
1233
|
-
comment =
|
|
1247
|
+
comment = `Zip files are piling up... anything to extract?`;
|
|
1234
1248
|
} else if (docs.length > 0) {
|
|
1235
|
-
comment =
|
|
1249
|
+
comment = `Working on documents~ keep it up!`;
|
|
1236
1250
|
} else if (files.length <= 3) {
|
|
1237
|
-
comment = '
|
|
1251
|
+
comment = 'Nice and tidy desktop~ feels good!';
|
|
1238
1252
|
}
|
|
1239
1253
|
|
|
1240
1254
|
if (comment) {
|
|
@@ -1244,22 +1258,22 @@ function handleDesktopCheck(now) {
|
|
|
1244
1258
|
emotion: 'curious',
|
|
1245
1259
|
});
|
|
1246
1260
|
lastSpeechTime = now;
|
|
1247
|
-
console.log(`[ClawMate]
|
|
1261
|
+
console.log(`[ClawMate] Desktop check: ${comment}`);
|
|
1248
1262
|
}
|
|
1249
1263
|
} catch {
|
|
1250
|
-
//
|
|
1264
|
+
// Desktop access failed -- ignore
|
|
1251
1265
|
}
|
|
1252
1266
|
}
|
|
1253
1267
|
|
|
1254
1268
|
/**
|
|
1255
|
-
*
|
|
1256
|
-
*
|
|
1269
|
+
* Screen observation (2 min interval, 10% chance)
|
|
1270
|
+
* Capture screenshot for AI to recognize screen content
|
|
1257
1271
|
*/
|
|
1258
1272
|
function handleScreenObservation(now) {
|
|
1259
|
-
const screenCheckInterval = 2 * 60 * 1000; // 2
|
|
1273
|
+
const screenCheckInterval = 2 * 60 * 1000; // 2 minutes
|
|
1260
1274
|
if (now - lastScreenCheckTime < screenCheckInterval) return;
|
|
1261
1275
|
|
|
1262
|
-
// 10%
|
|
1276
|
+
// Only run at 10% probability (resource saving)
|
|
1263
1277
|
if (Math.random() > 0.1) return;
|
|
1264
1278
|
|
|
1265
1279
|
lastScreenCheckTime = now;
|
|
@@ -1267,11 +1281,11 @@ function handleScreenObservation(now) {
|
|
|
1267
1281
|
if (!connector || !connector.connected) return;
|
|
1268
1282
|
|
|
1269
1283
|
connector.requestScreenCapture();
|
|
1270
|
-
console.log('[ClawMate]
|
|
1284
|
+
console.log('[ClawMate] Screen capture requested');
|
|
1271
1285
|
}
|
|
1272
1286
|
|
|
1273
1287
|
/**
|
|
1274
|
-
*
|
|
1288
|
+
* Weighted random selection
|
|
1275
1289
|
*/
|
|
1276
1290
|
function weightedRandom(items) {
|
|
1277
1291
|
const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
|
|
@@ -1286,29 +1300,29 @@ function weightedRandom(items) {
|
|
|
1286
1300
|
}
|
|
1287
1301
|
|
|
1288
1302
|
// =====================================================
|
|
1289
|
-
//
|
|
1303
|
+
// Spatial exploration system -- pet roams the computer like "home"
|
|
1290
1304
|
// =====================================================
|
|
1291
1305
|
|
|
1292
1306
|
/**
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1307
|
+
* Spatial exploration handler (20s interval, 20% chance)
|
|
1308
|
+
* Walk on windows, rappel down, return home, etc.
|
|
1295
1309
|
*/
|
|
1296
1310
|
function handleExploration(now, state) {
|
|
1297
|
-
const exploreInterval = 20000; // 20
|
|
1311
|
+
const exploreInterval = 20000; // 20 seconds
|
|
1298
1312
|
if (now - lastExploreTime < exploreInterval) return;
|
|
1299
1313
|
|
|
1300
|
-
//
|
|
1314
|
+
// Base 20% chance + explorationBias correction (positive bias = more exploration)
|
|
1301
1315
|
const exploreChance = Math.max(0.05, Math.min(0.8, 0.2 + behaviorAdjustments.explorationBias));
|
|
1302
1316
|
if (Math.random() > exploreChance) return;
|
|
1303
1317
|
lastExploreTime = now;
|
|
1304
1318
|
|
|
1305
|
-
//
|
|
1319
|
+
// Weighted exploration action selection
|
|
1306
1320
|
const actions = [
|
|
1307
|
-
{ type: 'jump_to_center', weight: 15, speech: '
|
|
1308
|
-
{ type: 'rappel_down', weight: 10, speech: '
|
|
1321
|
+
{ type: 'jump_to_center', weight: 15, speech: 'Exploring the center~!' },
|
|
1322
|
+
{ type: 'rappel_down', weight: 10, speech: 'Let me rappel down~' },
|
|
1309
1323
|
{ type: 'climb_wall', weight: 20 },
|
|
1310
|
-
{ type: 'visit_window', weight: 25, speech: '
|
|
1311
|
-
{ type: 'return_home', weight: 30, speech: '
|
|
1324
|
+
{ type: 'visit_window', weight: 25, speech: 'Should I climb on this window?' },
|
|
1325
|
+
{ type: 'return_home', weight: 30, speech: 'Let\'s go home~' },
|
|
1312
1326
|
];
|
|
1313
1327
|
|
|
1314
1328
|
const selected = weightedRandom(actions);
|
|
@@ -1330,7 +1344,7 @@ function handleExploration(now, state) {
|
|
|
1330
1344
|
break;
|
|
1331
1345
|
|
|
1332
1346
|
case 'visit_window':
|
|
1333
|
-
//
|
|
1347
|
+
// Pick a random known window and jump to its titlebar
|
|
1334
1348
|
if (knownWindows.length > 0) {
|
|
1335
1349
|
const win = knownWindows[Math.floor(Math.random() * knownWindows.length)];
|
|
1336
1350
|
connector.jumpTo(win.x + win.width / 2, win.y);
|
|
@@ -1348,7 +1362,7 @@ function handleExploration(now, state) {
|
|
|
1348
1362
|
break;
|
|
1349
1363
|
}
|
|
1350
1364
|
|
|
1351
|
-
//
|
|
1365
|
+
// Save exploration history (last 20)
|
|
1352
1366
|
explorationHistory.push({ type: selected.type, time: now });
|
|
1353
1367
|
if (explorationHistory.length > 20) {
|
|
1354
1368
|
explorationHistory.shift();
|
|
@@ -1356,25 +1370,25 @@ function handleExploration(now, state) {
|
|
|
1356
1370
|
}
|
|
1357
1371
|
|
|
1358
1372
|
/**
|
|
1359
|
-
*
|
|
1360
|
-
*
|
|
1373
|
+
* Periodic window position refresh (30s interval)
|
|
1374
|
+
* Get open window list from OS for exploration use
|
|
1361
1375
|
*/
|
|
1362
1376
|
function handleWindowCheck(now) {
|
|
1363
|
-
const windowCheckInterval = 30000; // 30
|
|
1377
|
+
const windowCheckInterval = 30000; // 30 seconds
|
|
1364
1378
|
if (now - lastWindowCheckTime < windowCheckInterval) return;
|
|
1365
1379
|
lastWindowCheckTime = now;
|
|
1366
1380
|
connector.queryWindows();
|
|
1367
1381
|
}
|
|
1368
1382
|
|
|
1369
1383
|
/**
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1384
|
+
* Desktop folder carry (3 min interval, 10% chance)
|
|
1385
|
+
* Pick up a desktop folder, carry it around briefly, then put it down
|
|
1372
1386
|
*/
|
|
1373
1387
|
function handleFolderCarry(now) {
|
|
1374
|
-
const carryInterval = 3 * 60 * 1000; // 3
|
|
1388
|
+
const carryInterval = 3 * 60 * 1000; // 3 minutes
|
|
1375
1389
|
if (now - lastFolderCarryTime < carryInterval) return;
|
|
1376
1390
|
|
|
1377
|
-
// 10%
|
|
1391
|
+
// 10% chance
|
|
1378
1392
|
if (Math.random() > 0.1) return;
|
|
1379
1393
|
lastFolderCarryTime = now;
|
|
1380
1394
|
|
|
@@ -1383,7 +1397,7 @@ function handleFolderCarry(now) {
|
|
|
1383
1397
|
if (!fs.existsSync(desktopPath)) return;
|
|
1384
1398
|
|
|
1385
1399
|
const entries = fs.readdirSync(desktopPath, { withFileTypes: true });
|
|
1386
|
-
//
|
|
1400
|
+
// Filter folders only (exclude hidden folders, safe ones only)
|
|
1387
1401
|
const folders = entries
|
|
1388
1402
|
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
1389
1403
|
.map(e => e.name);
|
|
@@ -1393,31 +1407,200 @@ function handleFolderCarry(now) {
|
|
|
1393
1407
|
const folder = folders[Math.floor(Math.random() * folders.length)];
|
|
1394
1408
|
connector.decide({
|
|
1395
1409
|
action: 'carrying',
|
|
1396
|
-
speech:
|
|
1410
|
+
speech: `Let me carry the ${folder} folder around~`,
|
|
1397
1411
|
emotion: 'playful',
|
|
1398
1412
|
});
|
|
1399
1413
|
connector.carryFile(folder);
|
|
1400
1414
|
|
|
1401
|
-
// 5
|
|
1415
|
+
// Put it down after 5 seconds
|
|
1402
1416
|
setTimeout(() => {
|
|
1403
1417
|
if (connector && connector.connected) {
|
|
1404
1418
|
connector.dropFile();
|
|
1405
|
-
connector.speak('
|
|
1419
|
+
connector.speak('I\'ll leave it here~');
|
|
1406
1420
|
}
|
|
1407
1421
|
}, 5000);
|
|
1408
1422
|
} catch {
|
|
1409
|
-
//
|
|
1423
|
+
// Desktop folder access failed -- ignore
|
|
1410
1424
|
}
|
|
1411
1425
|
}
|
|
1412
1426
|
|
|
1413
1427
|
// =====================================================
|
|
1414
|
-
//
|
|
1428
|
+
// AI motion generation system -- dynamically generate keyframe-based movement
|
|
1429
|
+
// =====================================================
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* AI motion generation handler (2 min interval, 15% chance)
|
|
1433
|
+
* AI directly generates and registers+executes custom movement patterns
|
|
1434
|
+
*
|
|
1435
|
+
* Generation strategy:
|
|
1436
|
+
* Attempt 1: Generate complete keyframe data via apiRef.generate()
|
|
1437
|
+
* Attempt 2: State-based procedural motion generation (fallback)
|
|
1438
|
+
*/
|
|
1439
|
+
async function handleMotionGeneration(now, state) {
|
|
1440
|
+
const motionGenInterval = 2 * 60 * 1000; // 2 minutes
|
|
1441
|
+
if (now - lastMotionGenTime < motionGenInterval) return;
|
|
1442
|
+
if (Math.random() > 0.15) return; // 15% chance
|
|
1443
|
+
lastMotionGenTime = now;
|
|
1444
|
+
|
|
1445
|
+
const currentState = state?.action || state?.state || 'idle';
|
|
1446
|
+
|
|
1447
|
+
// Attempt motion generation via AI
|
|
1448
|
+
let motionDef = null;
|
|
1449
|
+
if (apiRef?.generate) {
|
|
1450
|
+
try {
|
|
1451
|
+
motionDef = await generateMotionWithAI(currentState);
|
|
1452
|
+
} catch {}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// Fallback: procedural motion generation
|
|
1456
|
+
if (!motionDef) {
|
|
1457
|
+
motionDef = generateProceduralMotion(currentState, now);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (motionDef && connector?.connected) {
|
|
1461
|
+
const motionName = `ai_motion_${generatedMotionCount++}`;
|
|
1462
|
+
connector.registerMovement(motionName, motionDef);
|
|
1463
|
+
|
|
1464
|
+
// Execute after a short delay
|
|
1465
|
+
setTimeout(() => {
|
|
1466
|
+
if (connector?.connected) {
|
|
1467
|
+
connector.customMove(motionName, {});
|
|
1468
|
+
console.log(`[ClawMate] AI motion generated and executed: ${motionName}`);
|
|
1469
|
+
}
|
|
1470
|
+
}, 500);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Generate keyframe motion via AI
|
|
1476
|
+
* Generates motion definitions using formula or waypoints approach
|
|
1477
|
+
*/
|
|
1478
|
+
async function generateMotionWithAI(currentState) {
|
|
1479
|
+
const prompt = `Current pet state: ${currentState}.
|
|
1480
|
+
Create a fun movement pattern as JSON that fits this situation.
|
|
1481
|
+
|
|
1482
|
+
Choose one of two formats:
|
|
1483
|
+
1) formula approach (mathematical trajectory):
|
|
1484
|
+
{"type":"formula","formula":{"xAmp":80,"yAmp":40,"xFreq":1,"yFreq":2,"xPhase":0,"yPhase":0},"duration":3000,"speed":1.5}
|
|
1485
|
+
|
|
1486
|
+
2) waypoints approach (path points):
|
|
1487
|
+
{"type":"waypoints","waypoints":[{"x":100,"y":200,"pause":300},{"x":300,"y":100},{"x":500,"y":250}],"speed":2}
|
|
1488
|
+
|
|
1489
|
+
Rules:
|
|
1490
|
+
- xAmp/yAmp: 10~150 range (considering screen size)
|
|
1491
|
+
- duration: 2000~6000ms
|
|
1492
|
+
- waypoints: 3~6 points
|
|
1493
|
+
- speed: 0.5~3
|
|
1494
|
+
- Match pet personality: playful and cute movements
|
|
1495
|
+
Output JSON only.`;
|
|
1496
|
+
|
|
1497
|
+
const response = await apiRef.generate(prompt);
|
|
1498
|
+
if (!response || typeof response !== 'string') return null;
|
|
1499
|
+
|
|
1500
|
+
// JSON parsing
|
|
1501
|
+
let jsonStr = response;
|
|
1502
|
+
const jsonMatch = response.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
1503
|
+
if (jsonMatch) jsonStr = jsonMatch[1].trim();
|
|
1504
|
+
else {
|
|
1505
|
+
const braceMatch = response.match(/\{[\s\S]*\}/);
|
|
1506
|
+
if (braceMatch) jsonStr = braceMatch[0];
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
try {
|
|
1510
|
+
const def = JSON.parse(jsonStr);
|
|
1511
|
+
// Basic validation
|
|
1512
|
+
if (def.type === 'formula' && def.formula) {
|
|
1513
|
+
def.duration = Math.min(6000, Math.max(2000, def.duration || 3000));
|
|
1514
|
+
return def;
|
|
1515
|
+
}
|
|
1516
|
+
if (def.type === 'waypoints' && Array.isArray(def.waypoints) && def.waypoints.length >= 2) {
|
|
1517
|
+
return def;
|
|
1518
|
+
}
|
|
1519
|
+
} catch {}
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* Procedural motion generation (fallback when no AI)
|
|
1525
|
+
* Mathematically generate motion patterns based on current state and time
|
|
1526
|
+
*/
|
|
1527
|
+
function generateProceduralMotion(currentState, now) {
|
|
1528
|
+
const hour = new Date(now).getHours();
|
|
1529
|
+
const seed = now % 1000;
|
|
1530
|
+
|
|
1531
|
+
// Motion characteristics per state
|
|
1532
|
+
const stateMotions = {
|
|
1533
|
+
idle: () => {
|
|
1534
|
+
// Light side-to-side swaying or small circle
|
|
1535
|
+
if (seed > 500) {
|
|
1536
|
+
return {
|
|
1537
|
+
type: 'formula',
|
|
1538
|
+
formula: { xAmp: 20 + seed % 30, yAmp: 5 + seed % 10, xFreq: 0.5, yFreq: 1, xPhase: 0, yPhase: Math.PI / 2 },
|
|
1539
|
+
duration: 3000,
|
|
1540
|
+
speed: 0.8,
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
return {
|
|
1544
|
+
type: 'formula',
|
|
1545
|
+
formula: { xAmp: 15, yAmp: 15, xFreq: 1, yFreq: 1, xPhase: 0, yPhase: Math.PI / 2 },
|
|
1546
|
+
duration: 2500,
|
|
1547
|
+
speed: 0.6,
|
|
1548
|
+
};
|
|
1549
|
+
},
|
|
1550
|
+
walking: () => {
|
|
1551
|
+
// Zigzag or sine wave movement
|
|
1552
|
+
const amp = 30 + seed % 50;
|
|
1553
|
+
return {
|
|
1554
|
+
type: 'formula',
|
|
1555
|
+
formula: { xAmp: amp, yAmp: amp * 0.3, xFreq: 0.5, yFreq: 2, xPhase: 0, yPhase: 0 },
|
|
1556
|
+
duration: 4000,
|
|
1557
|
+
speed: 1.2,
|
|
1558
|
+
};
|
|
1559
|
+
},
|
|
1560
|
+
excited: () => {
|
|
1561
|
+
// Lively figure-8 trajectory
|
|
1562
|
+
return {
|
|
1563
|
+
type: 'formula',
|
|
1564
|
+
formula: { xAmp: 80 + seed % 40, yAmp: 40 + seed % 20, xFreq: 1, yFreq: 2, xPhase: 0, yPhase: 0 },
|
|
1565
|
+
duration: 3000,
|
|
1566
|
+
speed: 2.0,
|
|
1567
|
+
};
|
|
1568
|
+
},
|
|
1569
|
+
playing: () => {
|
|
1570
|
+
// Irregular waypoints (playful feel)
|
|
1571
|
+
const points = [];
|
|
1572
|
+
for (let i = 0; i < 4; i++) {
|
|
1573
|
+
points.push({
|
|
1574
|
+
x: 100 + Math.floor(Math.random() * 800),
|
|
1575
|
+
y: 100 + Math.floor(Math.random() * 400),
|
|
1576
|
+
pause: i === 0 ? 200 : 0,
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
return { type: 'waypoints', waypoints: points, speed: 2.5 };
|
|
1580
|
+
},
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1583
|
+
// Slow motion at night
|
|
1584
|
+
const isNight = hour >= 23 || hour < 6;
|
|
1585
|
+
const generator = stateMotions[currentState] || stateMotions.idle;
|
|
1586
|
+
const motion = generator();
|
|
1587
|
+
|
|
1588
|
+
if (isNight) {
|
|
1589
|
+
motion.speed = Math.min(0.5, (motion.speed || 1) * 0.4);
|
|
1590
|
+
if (motion.duration) motion.duration *= 1.5;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
return motion;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// =====================================================
|
|
1597
|
+
// Self-observation system (Metrics -> behavior adjustment)
|
|
1415
1598
|
// =====================================================
|
|
1416
1599
|
|
|
1417
1600
|
/**
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1601
|
+
* Handle incoming metrics data
|
|
1602
|
+
* Analyze behavior quality metrics sent from renderer every 30 seconds,
|
|
1603
|
+
* detect anomalies and auto-adjust behavior patterns.
|
|
1421
1604
|
*
|
|
1422
1605
|
* @param {object} data - { metrics: {...}, timestamp }
|
|
1423
1606
|
*/
|
|
@@ -1426,17 +1609,17 @@ function handleMetrics(data) {
|
|
|
1426
1609
|
const metrics = data.metrics;
|
|
1427
1610
|
latestMetrics = metrics;
|
|
1428
1611
|
|
|
1429
|
-
//
|
|
1612
|
+
// Maintain history (last 10)
|
|
1430
1613
|
metricsHistory.push(metrics);
|
|
1431
1614
|
if (metricsHistory.length > 10) metricsHistory.shift();
|
|
1432
1615
|
|
|
1433
|
-
//
|
|
1616
|
+
// Anomaly detection and response
|
|
1434
1617
|
_detectAnomalies(metrics);
|
|
1435
1618
|
|
|
1436
|
-
//
|
|
1619
|
+
// Auto-adjust behavior
|
|
1437
1620
|
adjustBehavior(metrics);
|
|
1438
1621
|
|
|
1439
|
-
//
|
|
1622
|
+
// Periodic quality report (console log every 5 min)
|
|
1440
1623
|
const now = Date.now();
|
|
1441
1624
|
if (now - lastMetricsLogTime >= 5 * 60 * 1000) {
|
|
1442
1625
|
lastMetricsLogTime = now;
|
|
@@ -1445,139 +1628,139 @@ function handleMetrics(data) {
|
|
|
1445
1628
|
}
|
|
1446
1629
|
|
|
1447
1630
|
/**
|
|
1448
|
-
*
|
|
1631
|
+
* Anomaly detection: respond immediately when metric thresholds are exceeded
|
|
1449
1632
|
*
|
|
1450
|
-
* - FPS < 30
|
|
1451
|
-
* - idle
|
|
1452
|
-
* -
|
|
1453
|
-
* -
|
|
1633
|
+
* - FPS < 30 -> performance warning, reduce action frequency
|
|
1634
|
+
* - idle ratio > 80% -> too stationary, encourage activity
|
|
1635
|
+
* - exploration coverage < 30% -> encourage exploring new areas
|
|
1636
|
+
* - user clicks 0 (for extended period) -> attention-seeking behavior
|
|
1454
1637
|
*/
|
|
1455
1638
|
function _detectAnomalies(metrics) {
|
|
1456
1639
|
if (!connector || !connector.connected) return;
|
|
1457
1640
|
|
|
1458
|
-
// --- FPS
|
|
1641
|
+
// --- FPS drop detection ---
|
|
1459
1642
|
if (metrics.fps < 30 && metrics.fps > 0) {
|
|
1460
|
-
console.log(`[ClawMate][Metrics] FPS
|
|
1461
|
-
connector.speak('
|
|
1643
|
+
console.log(`[ClawMate][Metrics] FPS drop detected: ${metrics.fps}`);
|
|
1644
|
+
connector.speak('Screen seems laggy... let me rest a bit.');
|
|
1462
1645
|
connector.action('idle');
|
|
1463
1646
|
|
|
1464
|
-
//
|
|
1647
|
+
// Immediately reduce action frequency to lower rendering load
|
|
1465
1648
|
behaviorAdjustments.actionCooldownMultiplier = 3.0;
|
|
1466
1649
|
behaviorAdjustments.speechCooldownMultiplier = 2.0;
|
|
1467
1650
|
behaviorAdjustments.activityLevel = 0.5;
|
|
1468
|
-
return; //
|
|
1651
|
+
return; // Defer other adjustments during FPS issues
|
|
1469
1652
|
}
|
|
1470
1653
|
|
|
1471
|
-
// --- idle
|
|
1654
|
+
// --- Excessive idle ratio ---
|
|
1472
1655
|
if (metrics.idleRatio > 0.8) {
|
|
1473
|
-
console.log(`[ClawMate][Metrics] idle
|
|
1656
|
+
console.log(`[ClawMate][Metrics] Excessive idle ratio: ${(metrics.idleRatio * 100).toFixed(0)}%`);
|
|
1474
1657
|
|
|
1475
|
-
// 10%
|
|
1658
|
+
// 10% chance for wake-up line (to avoid spam)
|
|
1476
1659
|
if (Math.random() < 0.1) {
|
|
1477
1660
|
const idleReactions = [
|
|
1478
|
-
'
|
|
1479
|
-
'
|
|
1480
|
-
'
|
|
1661
|
+
'Staying still is boring! Let me walk around~',
|
|
1662
|
+
'Was just spacing out... time to move!',
|
|
1663
|
+
'So bored~ let\'s go explore!',
|
|
1481
1664
|
];
|
|
1482
1665
|
const text = idleReactions[Math.floor(Math.random() * idleReactions.length)];
|
|
1483
1666
|
connector.speak(text);
|
|
1484
1667
|
}
|
|
1485
1668
|
}
|
|
1486
1669
|
|
|
1487
|
-
// ---
|
|
1670
|
+
// --- Insufficient exploration coverage ---
|
|
1488
1671
|
if (metrics.explorationCoverage < 0.3 && metrics.period >= 25000) {
|
|
1489
|
-
console.log(`[ClawMate][Metrics]
|
|
1672
|
+
console.log(`[ClawMate][Metrics] Low exploration coverage: ${(metrics.explorationCoverage * 100).toFixed(0)}%`);
|
|
1490
1673
|
|
|
1491
|
-
// 5%
|
|
1674
|
+
// 5% chance to encourage exploration (frequency control)
|
|
1492
1675
|
if (Math.random() < 0.05) {
|
|
1493
|
-
connector.speak('
|
|
1676
|
+
connector.speak('So many places I haven\'t been~ shall we explore!');
|
|
1494
1677
|
}
|
|
1495
1678
|
}
|
|
1496
1679
|
|
|
1497
|
-
// ---
|
|
1498
|
-
//
|
|
1680
|
+
// --- Decreased user interaction ---
|
|
1681
|
+
// If 0 clicks in last 3 consecutive reports, seek attention
|
|
1499
1682
|
if (metricsHistory.length >= 3) {
|
|
1500
1683
|
const recent3 = metricsHistory.slice(-3);
|
|
1501
1684
|
const noClicks = recent3.every(m => (m.userClicks || 0) === 0);
|
|
1502
1685
|
if (noClicks) {
|
|
1503
|
-
// 5%
|
|
1686
|
+
// 5% chance to seek attention (on consecutive detection)
|
|
1504
1687
|
if (Math.random() < 0.05) {
|
|
1505
1688
|
connector.decide({
|
|
1506
1689
|
action: 'excited',
|
|
1507
|
-
speech: '
|
|
1690
|
+
speech: 'I\'m right here~ click me if you\'re bored!',
|
|
1508
1691
|
emotion: 'playful',
|
|
1509
1692
|
});
|
|
1510
|
-
console.log('[ClawMate][Metrics]
|
|
1693
|
+
console.log('[ClawMate][Metrics] Decreased user interaction -> seeking attention');
|
|
1511
1694
|
}
|
|
1512
1695
|
}
|
|
1513
1696
|
}
|
|
1514
1697
|
}
|
|
1515
1698
|
|
|
1516
1699
|
/**
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1700
|
+
* Auto-adjust behavior patterns
|
|
1701
|
+
* Real-time tune action frequency/patterns based on metrics data.
|
|
1519
1702
|
*
|
|
1520
|
-
*
|
|
1521
|
-
* - FPS
|
|
1522
|
-
* - idle
|
|
1523
|
-
* -
|
|
1524
|
-
* -
|
|
1703
|
+
* Adjustment principles:
|
|
1704
|
+
* - Low FPS -> reduce action frequency to lower rendering load
|
|
1705
|
+
* - Too much idle -> increase activity
|
|
1706
|
+
* - Low exploration coverage -> increase exploration probability
|
|
1707
|
+
* - Active user interaction -> increase response frequency
|
|
1525
1708
|
*
|
|
1526
|
-
* @param {object} metrics -
|
|
1709
|
+
* @param {object} metrics - Current metrics data
|
|
1527
1710
|
*/
|
|
1528
1711
|
function adjustBehavior(metrics) {
|
|
1529
|
-
// --- FPS
|
|
1712
|
+
// --- FPS-based activity level adjustment ---
|
|
1530
1713
|
if (metrics.fps >= 50) {
|
|
1531
|
-
//
|
|
1714
|
+
// Sufficient performance -> normal activity
|
|
1532
1715
|
behaviorAdjustments.activityLevel = 1.0;
|
|
1533
1716
|
behaviorAdjustments.actionCooldownMultiplier = 1.0;
|
|
1534
1717
|
} else if (metrics.fps >= 30) {
|
|
1535
|
-
//
|
|
1718
|
+
// Slightly insufficient performance -> slightly reduce activity
|
|
1536
1719
|
behaviorAdjustments.activityLevel = 0.8;
|
|
1537
1720
|
behaviorAdjustments.actionCooldownMultiplier = 1.5;
|
|
1538
1721
|
} else {
|
|
1539
|
-
//
|
|
1722
|
+
// Insufficient performance -> greatly reduce activity (already handled in _detectAnomalies)
|
|
1540
1723
|
behaviorAdjustments.activityLevel = 0.5;
|
|
1541
1724
|
behaviorAdjustments.actionCooldownMultiplier = 3.0;
|
|
1542
1725
|
}
|
|
1543
1726
|
|
|
1544
|
-
// ---
|
|
1727
|
+
// --- Idle ratio based activity adjustment ---
|
|
1545
1728
|
if (metrics.idleRatio > 0.8) {
|
|
1546
|
-
//
|
|
1729
|
+
// Too stationary -> shorten action cooldown, increase activity level
|
|
1547
1730
|
behaviorAdjustments.actionCooldownMultiplier = Math.max(0.5,
|
|
1548
1731
|
behaviorAdjustments.actionCooldownMultiplier * 0.7);
|
|
1549
1732
|
behaviorAdjustments.activityLevel = Math.min(1.5,
|
|
1550
1733
|
behaviorAdjustments.activityLevel * 1.3);
|
|
1551
1734
|
} else if (metrics.idleRatio < 0.1) {
|
|
1552
|
-
//
|
|
1735
|
+
// Too busy -> let it rest a bit
|
|
1553
1736
|
behaviorAdjustments.actionCooldownMultiplier = Math.max(1.0,
|
|
1554
1737
|
behaviorAdjustments.actionCooldownMultiplier * 1.2);
|
|
1555
1738
|
}
|
|
1556
1739
|
|
|
1557
|
-
// ---
|
|
1740
|
+
// --- Exploration coverage based exploration bias ---
|
|
1558
1741
|
if (metrics.explorationCoverage < 0.3) {
|
|
1559
|
-
//
|
|
1742
|
+
// Insufficient exploration -> increase exploration probability
|
|
1560
1743
|
behaviorAdjustments.explorationBias = 0.15;
|
|
1561
1744
|
} else if (metrics.explorationCoverage > 0.7) {
|
|
1562
|
-
//
|
|
1745
|
+
// Explored enough -> reset exploration probability to default
|
|
1563
1746
|
behaviorAdjustments.explorationBias = 0;
|
|
1564
1747
|
} else {
|
|
1565
|
-
//
|
|
1748
|
+
// Medium -> slight increase
|
|
1566
1749
|
behaviorAdjustments.explorationBias = 0.05;
|
|
1567
1750
|
}
|
|
1568
1751
|
|
|
1569
|
-
// ---
|
|
1752
|
+
// --- User interaction based speech bubble frequency ---
|
|
1570
1753
|
if (metrics.userClicks > 3) {
|
|
1571
|
-
//
|
|
1754
|
+
// User actively clicking -> increase speech frequency (reactive)
|
|
1572
1755
|
behaviorAdjustments.speechCooldownMultiplier = 0.7;
|
|
1573
1756
|
} else if (metrics.userClicks === 0 && metrics.speechCount > 5) {
|
|
1574
|
-
//
|
|
1757
|
+
// User not responding but talking too much -> reduce speech
|
|
1575
1758
|
behaviorAdjustments.speechCooldownMultiplier = 1.5;
|
|
1576
1759
|
} else {
|
|
1577
1760
|
behaviorAdjustments.speechCooldownMultiplier = 1.0;
|
|
1578
1761
|
}
|
|
1579
1762
|
|
|
1580
|
-
//
|
|
1763
|
+
// Value range clamping (safety guard)
|
|
1581
1764
|
behaviorAdjustments.activityLevel = Math.max(0.3, Math.min(2.0, behaviorAdjustments.activityLevel));
|
|
1582
1765
|
behaviorAdjustments.actionCooldownMultiplier = Math.max(0.3, Math.min(5.0, behaviorAdjustments.actionCooldownMultiplier));
|
|
1583
1766
|
behaviorAdjustments.speechCooldownMultiplier = Math.max(0.3, Math.min(5.0, behaviorAdjustments.speechCooldownMultiplier));
|
|
@@ -1585,27 +1768,27 @@ function adjustBehavior(metrics) {
|
|
|
1585
1768
|
}
|
|
1586
1769
|
|
|
1587
1770
|
/**
|
|
1588
|
-
*
|
|
1589
|
-
*
|
|
1771
|
+
* Quality report console output (every 5 minutes)
|
|
1772
|
+
* Allows developers/operators to monitor pet behavior quality.
|
|
1590
1773
|
*/
|
|
1591
1774
|
function _logQualityReport(metrics) {
|
|
1592
1775
|
const adj = behaviorAdjustments;
|
|
1593
|
-
console.log('=== [ClawMate]
|
|
1594
|
-
console.log(` FPS: ${metrics.fps} |
|
|
1595
|
-
console.log(`
|
|
1596
|
-
console.log(`
|
|
1597
|
-
console.log(`
|
|
1598
|
-
console.log(` [
|
|
1599
|
-
console.log('
|
|
1776
|
+
console.log('=== [ClawMate] Behavior Quality Report ===');
|
|
1777
|
+
console.log(` FPS: ${metrics.fps} | Frame consistency: ${metrics.animationFrameConsistency}`);
|
|
1778
|
+
console.log(` Movement smoothness: ${metrics.movementSmoothness} | Wall contact: ${metrics.wallContactAccuracy}`);
|
|
1779
|
+
console.log(` Idle ratio: ${(metrics.idleRatio * 100).toFixed(0)}% | Exploration coverage: ${(metrics.explorationCoverage * 100).toFixed(0)}%`);
|
|
1780
|
+
console.log(` Response time: ${metrics.interactionResponseMs}ms | Speech: ${metrics.speechCount}x | Clicks: ${metrics.userClicks}x`);
|
|
1781
|
+
console.log(` [Adjustments] Activity: ${adj.activityLevel.toFixed(2)} | Action cooldown: x${adj.actionCooldownMultiplier.toFixed(2)} | Speech cooldown: x${adj.speechCooldownMultiplier.toFixed(2)} | Exploration bias: ${adj.explorationBias.toFixed(2)}`);
|
|
1782
|
+
console.log('==========================================');
|
|
1600
1783
|
}
|
|
1601
1784
|
|
|
1602
1785
|
// =====================================================
|
|
1603
|
-
// npm
|
|
1786
|
+
// npm package version check (for npm install -g users)
|
|
1604
1787
|
// =====================================================
|
|
1605
1788
|
|
|
1606
1789
|
/**
|
|
1607
|
-
*
|
|
1608
|
-
*
|
|
1790
|
+
* Check latest version from npm registry,
|
|
1791
|
+
* notify via console + pet speech bubble if different from current
|
|
1609
1792
|
*/
|
|
1610
1793
|
async function checkNpmUpdate() {
|
|
1611
1794
|
try {
|
|
@@ -1617,13 +1800,13 @@ async function checkNpmUpdate() {
|
|
|
1617
1800
|
const current = require('./package.json').version;
|
|
1618
1801
|
|
|
1619
1802
|
if (latest !== current) {
|
|
1620
|
-
console.log(`[ClawMate]
|
|
1621
|
-
console.log('[ClawMate]
|
|
1803
|
+
console.log(`[ClawMate] New version ${latest} available (current: ${current})`);
|
|
1804
|
+
console.log('[ClawMate] Update: npm update -g clawmate');
|
|
1622
1805
|
if (connector && connector.connected) {
|
|
1623
|
-
connector.speak(
|
|
1806
|
+
connector.speak(`Update available! v${latest}`);
|
|
1624
1807
|
}
|
|
1625
1808
|
}
|
|
1626
1809
|
} catch {
|
|
1627
|
-
// npm registry
|
|
1810
|
+
// npm registry access failed -- ignore (offline, etc.)
|
|
1628
1811
|
}
|
|
1629
1812
|
}
|