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