clawmate 1.0.1 → 1.2.0
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 +11 -0
- package/index.js +541 -2
- package/main/ai-bridge.js +45 -0
- package/main/ai-connector.js +78 -3
- package/main/autostart.js +45 -0
- package/main/index.js +44 -1
- package/main/ipc-handlers.js +36 -1
- package/main/platform.js +185 -2
- package/main/updater.js +51 -0
- package/package.json +4 -2
- package/preload/preload.js +8 -0
- package/renderer/css/pet.css +11 -0
- package/renderer/index.html +4 -0
- package/renderer/js/ai-controller.js +39 -1
- package/renderer/js/app.js +1 -1
- package/renderer/js/character.js +29 -26
- package/renderer/js/interactions.js +24 -3
- package/renderer/js/mode-manager.js +2 -2
- package/renderer/js/pet-engine.js +752 -89
- package/renderer/js/speech.js +52 -5
- package/renderer/js/state-machine.js +24 -2
package/electron-builder.yml
CHANGED
|
@@ -21,3 +21,14 @@ mac:
|
|
|
21
21
|
- dmg
|
|
22
22
|
icon: assets/icons/clawmate.icns
|
|
23
23
|
category: public.app-category.entertainment
|
|
24
|
+
linux:
|
|
25
|
+
target:
|
|
26
|
+
- AppImage
|
|
27
|
+
- deb
|
|
28
|
+
- snap
|
|
29
|
+
icon: assets/icons/clawmate.png
|
|
30
|
+
category: Entertainment
|
|
31
|
+
publish:
|
|
32
|
+
provider: github
|
|
33
|
+
owner: boqum
|
|
34
|
+
repo: clawmate
|
package/index.js
CHANGED
|
@@ -20,10 +20,28 @@ let connector = null;
|
|
|
20
20
|
let electronProcess = null;
|
|
21
21
|
let apiRef = null;
|
|
22
22
|
|
|
23
|
+
// =====================================================
|
|
24
|
+
// Think Loop 상태 관리
|
|
25
|
+
// =====================================================
|
|
26
|
+
let thinkTimer = null;
|
|
27
|
+
let lastSpeechTime = 0;
|
|
28
|
+
let lastActionTime = 0;
|
|
29
|
+
let lastDesktopCheckTime = 0;
|
|
30
|
+
let lastScreenCheckTime = 0;
|
|
31
|
+
let lastGreetingDate = null; // 하루에 한번만 인사
|
|
32
|
+
|
|
33
|
+
// 공간 탐험 시스템 상태
|
|
34
|
+
let knownWindows = []; // 알고 있는 윈도우 목록
|
|
35
|
+
let lastWindowCheckTime = 0;
|
|
36
|
+
let homePosition = null; // "집" 위치 (자주 가는 곳)
|
|
37
|
+
let explorationHistory = []; // 탐험한 위치 기록
|
|
38
|
+
let lastExploreTime = 0;
|
|
39
|
+
let lastFolderCarryTime = 0;
|
|
40
|
+
|
|
23
41
|
module.exports = {
|
|
24
42
|
id: 'clawmate',
|
|
25
43
|
name: 'ClawMate',
|
|
26
|
-
version: '1.
|
|
44
|
+
version: '1.2.0',
|
|
27
45
|
description: 'OpenClaw 데스크톱 펫 - AI가 조종하는 살아있는 Claw',
|
|
28
46
|
|
|
29
47
|
/**
|
|
@@ -34,6 +52,10 @@ module.exports = {
|
|
|
34
52
|
apiRef = api;
|
|
35
53
|
console.log('[ClawMate] 플러그인 초기화 — 자동 연결 시작');
|
|
36
54
|
autoConnect();
|
|
55
|
+
|
|
56
|
+
// npm 패키지 버전 체크 (최초 1회 + 24시간 간격)
|
|
57
|
+
checkNpmUpdate();
|
|
58
|
+
setInterval(checkNpmUpdate, 24 * 60 * 60 * 1000);
|
|
37
59
|
},
|
|
38
60
|
|
|
39
61
|
register(api) {
|
|
@@ -101,6 +123,7 @@ module.exports = {
|
|
|
101
123
|
*/
|
|
102
124
|
async destroy() {
|
|
103
125
|
console.log('[ClawMate] 플러그인 정리');
|
|
126
|
+
stopThinkLoop();
|
|
104
127
|
if (connector) {
|
|
105
128
|
connector.disconnect();
|
|
106
129
|
connector = null;
|
|
@@ -208,8 +231,14 @@ function setupConnectorEvents() {
|
|
|
208
231
|
await handleUserEvent(event);
|
|
209
232
|
});
|
|
210
233
|
|
|
234
|
+
// 윈도우 위치 정보 수신 → 탐험 시스템에서 활용
|
|
235
|
+
connector.on('window_positions', (data) => {
|
|
236
|
+
knownWindows = data.windows || [];
|
|
237
|
+
});
|
|
238
|
+
|
|
211
239
|
connector.on('disconnected', () => {
|
|
212
|
-
console.log('[ClawMate] 연결 끊김 — 재연결 시도');
|
|
240
|
+
console.log('[ClawMate] 연결 끊김 — Think Loop 중단, 재연결 시도');
|
|
241
|
+
stopThinkLoop();
|
|
213
242
|
startBackgroundReconnect();
|
|
214
243
|
});
|
|
215
244
|
|
|
@@ -225,6 +254,14 @@ function onConnected() {
|
|
|
225
254
|
if (connector && connector.connected) {
|
|
226
255
|
connector.speak('OpenClaw 연결됨! 같이 놀자!');
|
|
227
256
|
connector.action('excited');
|
|
257
|
+
|
|
258
|
+
// "집" 위치 설정 — 화면 하단 왼쪽을 기본 홈으로
|
|
259
|
+
homePosition = { x: 100, y: 1000, edge: 'bottom' };
|
|
260
|
+
|
|
261
|
+
// 초기 윈도우 목록 조회
|
|
262
|
+
connector.queryWindows();
|
|
263
|
+
|
|
264
|
+
startThinkLoop();
|
|
228
265
|
}
|
|
229
266
|
}
|
|
230
267
|
|
|
@@ -344,3 +381,505 @@ async function handleUserEvent(event) {
|
|
|
344
381
|
function sleep(ms) {
|
|
345
382
|
return new Promise(r => setTimeout(r, ms));
|
|
346
383
|
}
|
|
384
|
+
|
|
385
|
+
// =====================================================
|
|
386
|
+
// AI Think Loop — 주기적 자율 사고 시스템
|
|
387
|
+
// =====================================================
|
|
388
|
+
|
|
389
|
+
// 시간대별 인사말
|
|
390
|
+
const TIME_GREETINGS = {
|
|
391
|
+
morning: [
|
|
392
|
+
'좋은 아침! 오늘 하루도 화이팅!',
|
|
393
|
+
'일어났어? 커피 한 잔 어때?',
|
|
394
|
+
'모닝~ 오늘 날씨 어떨까?',
|
|
395
|
+
],
|
|
396
|
+
lunch: [
|
|
397
|
+
'점심 시간이다! 뭐 먹을 거야?',
|
|
398
|
+
'밥 먹었어? 건강이 최고야!',
|
|
399
|
+
'슬슬 배고프지 않아?',
|
|
400
|
+
],
|
|
401
|
+
evening: [
|
|
402
|
+
'오늘 하루 수고했어!',
|
|
403
|
+
'저녁이네~ 오늘 뭐 했어?',
|
|
404
|
+
'하루가 벌써 이렇게 지나가다니...',
|
|
405
|
+
],
|
|
406
|
+
night: [
|
|
407
|
+
'이 시간까지 깨있는 거야? 곧 자야지~',
|
|
408
|
+
'밤이 깊었어... 내일도 있잖아.',
|
|
409
|
+
'나는 슬슬 졸리다... zzZ',
|
|
410
|
+
],
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// 한가할 때 혼잣말 목록
|
|
414
|
+
const IDLE_CHATTER = [
|
|
415
|
+
'음~ 뭐 하고 놀까...',
|
|
416
|
+
'심심하다...',
|
|
417
|
+
'나 여기 있는 거 알지?',
|
|
418
|
+
'바탕화면 구경 중~',
|
|
419
|
+
'오늘 기분이 좋다!',
|
|
420
|
+
'후후, 잠깐 스트레칭~',
|
|
421
|
+
'이리저리 돌아다녀볼까~',
|
|
422
|
+
'혼자 놀기 프로...',
|
|
423
|
+
'주인님 뭐 하고 있는 거야~?',
|
|
424
|
+
'나한테 관심 좀 줘봐!',
|
|
425
|
+
'데스크톱이 넓고 좋다~',
|
|
426
|
+
'여기서 보이는 게 다 내 세상!',
|
|
427
|
+
// 공간 탐험 관련 멘트
|
|
428
|
+
'화면 위를 점프해볼까~!',
|
|
429
|
+
'천장에서 내려가보자!',
|
|
430
|
+
'이 창 위에 올라가봐야지~',
|
|
431
|
+
'여기가 내 집이야~ 편하다!',
|
|
432
|
+
'좀 돌아다녀볼까? 탐험 모드!',
|
|
433
|
+
];
|
|
434
|
+
|
|
435
|
+
// 랜덤 행동 목록
|
|
436
|
+
const RANDOM_ACTIONS = [
|
|
437
|
+
{ action: 'walking', weight: 30, minInterval: 5000 },
|
|
438
|
+
{ action: 'idle', weight: 25, minInterval: 3000 },
|
|
439
|
+
{ action: 'excited', weight: 10, minInterval: 15000 },
|
|
440
|
+
{ action: 'climbing', weight: 8, minInterval: 20000 },
|
|
441
|
+
{ action: 'looking_around', weight: 20, minInterval: 8000 },
|
|
442
|
+
{ action: 'sleeping', weight: 7, minInterval: 60000 },
|
|
443
|
+
// 공간 이동 행동
|
|
444
|
+
{ action: 'jumping', weight: 5, minInterval: 30000 },
|
|
445
|
+
{ action: 'rappelling', weight: 3, minInterval: 45000 },
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Think Loop 시작
|
|
450
|
+
* 3초 간격으로 AI가 자율적으로 사고하고 행동을 결정
|
|
451
|
+
*/
|
|
452
|
+
function startThinkLoop() {
|
|
453
|
+
if (thinkTimer) return;
|
|
454
|
+
console.log('[ClawMate] Think Loop 시작 — 3초 간격 자율 사고');
|
|
455
|
+
|
|
456
|
+
// 초기 타임스탬프 설정 (시작 직후 스팸 방지)
|
|
457
|
+
const now = Date.now();
|
|
458
|
+
lastSpeechTime = now;
|
|
459
|
+
lastActionTime = now;
|
|
460
|
+
lastDesktopCheckTime = now;
|
|
461
|
+
lastScreenCheckTime = now;
|
|
462
|
+
|
|
463
|
+
thinkTimer = setInterval(async () => {
|
|
464
|
+
try {
|
|
465
|
+
await thinkCycle();
|
|
466
|
+
} catch (err) {
|
|
467
|
+
console.error('[ClawMate] Think Loop 오류:', err.message);
|
|
468
|
+
}
|
|
469
|
+
}, 3000);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Think Loop 중단
|
|
474
|
+
*/
|
|
475
|
+
function stopThinkLoop() {
|
|
476
|
+
if (thinkTimer) {
|
|
477
|
+
clearInterval(thinkTimer);
|
|
478
|
+
thinkTimer = null;
|
|
479
|
+
console.log('[ClawMate] Think Loop 중단');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* 단일 사고 사이클 — 매 3초마다 실행
|
|
485
|
+
*/
|
|
486
|
+
async function thinkCycle() {
|
|
487
|
+
if (!connector || !connector.connected) return;
|
|
488
|
+
|
|
489
|
+
const now = Date.now();
|
|
490
|
+
const date = new Date();
|
|
491
|
+
const hour = date.getHours();
|
|
492
|
+
const todayStr = date.toISOString().slice(0, 10);
|
|
493
|
+
|
|
494
|
+
// 펫 상태 조회 (캐시된 값 또는 실시간)
|
|
495
|
+
const state = await connector.queryState(1500);
|
|
496
|
+
|
|
497
|
+
// --- 1) 시간대별 인사 (하루에 한번씩, 시간대별) ---
|
|
498
|
+
const greetingHandled = handleTimeGreeting(now, hour, todayStr);
|
|
499
|
+
|
|
500
|
+
// --- 2) 야간 수면 모드 (23시~5시: 말/행동 빈도 대폭 감소) ---
|
|
501
|
+
const isNightMode = hour >= 23 || hour < 5;
|
|
502
|
+
|
|
503
|
+
// --- 3) 자율 발화 (30초 쿨타임 + 확률) ---
|
|
504
|
+
if (!greetingHandled) {
|
|
505
|
+
handleIdleSpeech(now, isNightMode);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// --- 4) 자율 행동 결정 (5초 쿨타임 + 확률) ---
|
|
509
|
+
handleRandomAction(now, hour, isNightMode, state);
|
|
510
|
+
|
|
511
|
+
// --- 5) 바탕화면 파일 체크 (5분 간격) ---
|
|
512
|
+
handleDesktopCheck(now);
|
|
513
|
+
|
|
514
|
+
// --- 6) 화면 관찰 (2분 간격, 10% 확률) ---
|
|
515
|
+
handleScreenObservation(now);
|
|
516
|
+
|
|
517
|
+
// --- 7) 공간 탐험 (20초 간격, 20% 확률) ---
|
|
518
|
+
handleExploration(now, state);
|
|
519
|
+
|
|
520
|
+
// --- 8) 윈도우 체크 (30초 간격) ---
|
|
521
|
+
handleWindowCheck(now);
|
|
522
|
+
|
|
523
|
+
// --- 9) 바탕화면 폴더 나르기 (3분 간격, 10% 확률) ---
|
|
524
|
+
handleFolderCarry(now);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* 시간대별 인사 처리
|
|
529
|
+
* 아침/점심/저녁/밤 각각 하루 한 번
|
|
530
|
+
*/
|
|
531
|
+
function handleTimeGreeting(now, hour, todayStr) {
|
|
532
|
+
// 시간대 결정
|
|
533
|
+
let period = null;
|
|
534
|
+
if (hour >= 6 && hour < 9) period = 'morning';
|
|
535
|
+
else if (hour >= 11 && hour < 13) period = 'lunch';
|
|
536
|
+
else if (hour >= 17 && hour < 19) period = 'evening';
|
|
537
|
+
else if (hour >= 22 && hour < 24) period = 'night';
|
|
538
|
+
|
|
539
|
+
if (!period) return false;
|
|
540
|
+
|
|
541
|
+
const greetingKey = `${todayStr}_${period}`;
|
|
542
|
+
if (lastGreetingDate === greetingKey) return false;
|
|
543
|
+
|
|
544
|
+
// 시간대 인사 전송
|
|
545
|
+
lastGreetingDate = greetingKey;
|
|
546
|
+
const greetings = TIME_GREETINGS[period];
|
|
547
|
+
const text = greetings[Math.floor(Math.random() * greetings.length)];
|
|
548
|
+
|
|
549
|
+
const emotionMap = {
|
|
550
|
+
morning: 'happy',
|
|
551
|
+
lunch: 'curious',
|
|
552
|
+
evening: 'content',
|
|
553
|
+
night: 'sleepy',
|
|
554
|
+
};
|
|
555
|
+
const actionMap = {
|
|
556
|
+
morning: 'excited',
|
|
557
|
+
lunch: 'walking',
|
|
558
|
+
evening: 'idle',
|
|
559
|
+
night: 'sleeping',
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
connector.decide({
|
|
563
|
+
action: actionMap[period],
|
|
564
|
+
speech: text,
|
|
565
|
+
emotion: emotionMap[period],
|
|
566
|
+
});
|
|
567
|
+
lastSpeechTime = Date.now();
|
|
568
|
+
console.log(`[ClawMate] 시간대 인사 (${period}): ${text}`);
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* 한가할 때 혼잣말
|
|
574
|
+
* 최소 30초 쿨타임, 야간에는 확률 대폭 감소
|
|
575
|
+
*/
|
|
576
|
+
function handleIdleSpeech(now, isNightMode) {
|
|
577
|
+
const speechCooldown = 30000; // 30초
|
|
578
|
+
if (now - lastSpeechTime < speechCooldown) return;
|
|
579
|
+
|
|
580
|
+
// 야간: 5% 확률 / 주간: 25% 확률
|
|
581
|
+
const speechChance = isNightMode ? 0.05 : 0.25;
|
|
582
|
+
if (Math.random() > speechChance) return;
|
|
583
|
+
|
|
584
|
+
const text = IDLE_CHATTER[Math.floor(Math.random() * IDLE_CHATTER.length)];
|
|
585
|
+
connector.speak(text);
|
|
586
|
+
lastSpeechTime = now;
|
|
587
|
+
console.log(`[ClawMate] 혼잣말: ${text}`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* 자율 행동 결정
|
|
592
|
+
* 최소 5초 쿨타임, 가중치 기반 랜덤 선택
|
|
593
|
+
*/
|
|
594
|
+
function handleRandomAction(now, hour, isNightMode, state) {
|
|
595
|
+
const actionCooldown = 5000; // 5초
|
|
596
|
+
if (now - lastActionTime < actionCooldown) return;
|
|
597
|
+
|
|
598
|
+
// 야간: 10% 확률 / 주간: 40% 확률
|
|
599
|
+
const actionChance = isNightMode ? 0.1 : 0.4;
|
|
600
|
+
if (Math.random() > actionChance) return;
|
|
601
|
+
|
|
602
|
+
// 야간에는 sleeping 가중치 대폭 상승
|
|
603
|
+
const actions = RANDOM_ACTIONS.map(a => {
|
|
604
|
+
let weight = a.weight;
|
|
605
|
+
if (isNightMode) {
|
|
606
|
+
if (a.action === 'sleeping') weight = 60;
|
|
607
|
+
else if (a.action === 'excited' || a.action === 'climbing') weight = 2;
|
|
608
|
+
}
|
|
609
|
+
// 새벽/아침에는 looking_around 선호
|
|
610
|
+
if (hour >= 6 && hour < 9 && a.action === 'looking_around') weight += 15;
|
|
611
|
+
return { ...a, weight };
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// 최근 동일 행동 반복 방지: 현재 상태와 같으면 가중치 감소
|
|
615
|
+
const currentAction = state?.action || state?.state;
|
|
616
|
+
if (currentAction) {
|
|
617
|
+
const match = actions.find(a => a.action === currentAction);
|
|
618
|
+
if (match) match.weight = Math.max(1, Math.floor(match.weight * 0.3));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const selected = weightedRandom(actions);
|
|
622
|
+
if (!selected) return;
|
|
623
|
+
|
|
624
|
+
// minInterval 체크
|
|
625
|
+
if (now - lastActionTime < selected.minInterval) return;
|
|
626
|
+
|
|
627
|
+
// 공간 이동 행동은 전용 API로 처리
|
|
628
|
+
if (selected.action === 'jumping') {
|
|
629
|
+
// 랜덤 위치로 점프 또는 화면 중앙으로
|
|
630
|
+
if (Math.random() > 0.5) {
|
|
631
|
+
connector.moveToCenter();
|
|
632
|
+
} else {
|
|
633
|
+
const randomX = Math.floor(Math.random() * 1200) + 100;
|
|
634
|
+
const randomY = Math.floor(Math.random() * 800) + 100;
|
|
635
|
+
connector.jumpTo(randomX, randomY);
|
|
636
|
+
}
|
|
637
|
+
} else if (selected.action === 'rappelling') {
|
|
638
|
+
connector.rappel();
|
|
639
|
+
} else {
|
|
640
|
+
connector.action(selected.action);
|
|
641
|
+
}
|
|
642
|
+
lastActionTime = now;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* 바탕화면 파일 체크 (5분 간격)
|
|
647
|
+
* 데스크톱 폴더를 읽어서 재밌는 코멘트
|
|
648
|
+
*/
|
|
649
|
+
function handleDesktopCheck(now) {
|
|
650
|
+
const checkInterval = 5 * 60 * 1000; // 5분
|
|
651
|
+
if (now - lastDesktopCheckTime < checkInterval) return;
|
|
652
|
+
lastDesktopCheckTime = now;
|
|
653
|
+
|
|
654
|
+
// 15% 확률로만 실행 (매번 할 필요 없음)
|
|
655
|
+
if (Math.random() > 0.15) return;
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
const desktopPath = path.join(os.homedir(), 'Desktop');
|
|
659
|
+
if (!fs.existsSync(desktopPath)) return;
|
|
660
|
+
|
|
661
|
+
const files = fs.readdirSync(desktopPath);
|
|
662
|
+
if (files.length === 0) {
|
|
663
|
+
connector.speak('바탕화면이 깨끗하네! 좋아!');
|
|
664
|
+
lastSpeechTime = now;
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// 파일 종류별 코멘트
|
|
669
|
+
const images = files.filter(f => /\.(png|jpg|jpeg|gif|bmp|webp)$/i.test(f));
|
|
670
|
+
const docs = files.filter(f => /\.(pdf|doc|docx|xlsx|pptx|txt|hwp)$/i.test(f));
|
|
671
|
+
const zips = files.filter(f => /\.(zip|rar|7z|tar|gz)$/i.test(f));
|
|
672
|
+
|
|
673
|
+
let comment = null;
|
|
674
|
+
if (files.length > 20) {
|
|
675
|
+
comment = `바탕화면에 파일이 ${files.length}개나 있어! 정리 좀 할까?`;
|
|
676
|
+
} else if (images.length > 5) {
|
|
677
|
+
comment = `사진이 많네~ ${images.length}개나! 앨범 정리 어때?`;
|
|
678
|
+
} else if (zips.length > 3) {
|
|
679
|
+
comment = `압축 파일이 좀 쌓였네... 풀어볼 거 있어?`;
|
|
680
|
+
} else if (docs.length > 0) {
|
|
681
|
+
comment = `문서 작업 중이구나~ 화이팅!`;
|
|
682
|
+
} else if (files.length <= 3) {
|
|
683
|
+
comment = '바탕화면이 깔끔해서 기분 좋다~';
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (comment) {
|
|
687
|
+
connector.decide({
|
|
688
|
+
action: 'looking_around',
|
|
689
|
+
speech: comment,
|
|
690
|
+
emotion: 'curious',
|
|
691
|
+
});
|
|
692
|
+
lastSpeechTime = now;
|
|
693
|
+
console.log(`[ClawMate] 바탕화면 체크: ${comment}`);
|
|
694
|
+
}
|
|
695
|
+
} catch {
|
|
696
|
+
// 데스크톱 접근 실패 — 무시
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* 화면 관찰 (2분 간격, 10% 확률)
|
|
702
|
+
* 스크린샷을 캡처해서 OpenClaw AI가 화면 내용을 인식
|
|
703
|
+
*/
|
|
704
|
+
function handleScreenObservation(now) {
|
|
705
|
+
const screenCheckInterval = 2 * 60 * 1000; // 2분
|
|
706
|
+
if (now - lastScreenCheckTime < screenCheckInterval) return;
|
|
707
|
+
|
|
708
|
+
// 10% 확률로만 실행 (리소스 절약)
|
|
709
|
+
if (Math.random() > 0.1) return;
|
|
710
|
+
|
|
711
|
+
lastScreenCheckTime = now;
|
|
712
|
+
|
|
713
|
+
if (!connector || !connector.connected) return;
|
|
714
|
+
|
|
715
|
+
connector.requestScreenCapture();
|
|
716
|
+
console.log('[ClawMate] 화면 캡처 요청');
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* 가중치 기반 랜덤 선택
|
|
721
|
+
*/
|
|
722
|
+
function weightedRandom(items) {
|
|
723
|
+
const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
|
|
724
|
+
if (totalWeight <= 0) return null;
|
|
725
|
+
|
|
726
|
+
let random = Math.random() * totalWeight;
|
|
727
|
+
for (const item of items) {
|
|
728
|
+
random -= item.weight;
|
|
729
|
+
if (random <= 0) return item;
|
|
730
|
+
}
|
|
731
|
+
return items[items.length - 1];
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// =====================================================
|
|
735
|
+
// 공간 탐험 시스템 — OpenClaw이 컴퓨터를 "집"처럼 돌아다님
|
|
736
|
+
// =====================================================
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* 공간 탐험 처리 (20초 간격, 20% 확률)
|
|
740
|
+
* 윈도우 위를 걸어다니고, 레펠로 내려가고, 집으로 돌아가는 등
|
|
741
|
+
*/
|
|
742
|
+
function handleExploration(now, state) {
|
|
743
|
+
const exploreInterval = 20000; // 20초
|
|
744
|
+
if (now - lastExploreTime < exploreInterval) return;
|
|
745
|
+
|
|
746
|
+
// 20% 확률
|
|
747
|
+
if (Math.random() > 0.2) return;
|
|
748
|
+
lastExploreTime = now;
|
|
749
|
+
|
|
750
|
+
// 가중치 기반 탐험 행동 선택
|
|
751
|
+
const actions = [
|
|
752
|
+
{ type: 'jump_to_center', weight: 15, speech: '화면 중앙 탐험~!' },
|
|
753
|
+
{ type: 'rappel_down', weight: 10, speech: '실 타고 내려가볼까~' },
|
|
754
|
+
{ type: 'climb_wall', weight: 20 },
|
|
755
|
+
{ type: 'visit_window', weight: 25, speech: '이 창 위에 올라가볼까?' },
|
|
756
|
+
{ type: 'return_home', weight: 30, speech: '집에 가자~' },
|
|
757
|
+
];
|
|
758
|
+
|
|
759
|
+
const selected = weightedRandom(actions);
|
|
760
|
+
if (!selected) return;
|
|
761
|
+
|
|
762
|
+
switch (selected.type) {
|
|
763
|
+
case 'jump_to_center':
|
|
764
|
+
connector.moveToCenter();
|
|
765
|
+
if (selected.speech) connector.speak(selected.speech);
|
|
766
|
+
break;
|
|
767
|
+
|
|
768
|
+
case 'rappel_down':
|
|
769
|
+
connector.rappel();
|
|
770
|
+
if (selected.speech) setTimeout(() => connector.speak(selected.speech), 500);
|
|
771
|
+
break;
|
|
772
|
+
|
|
773
|
+
case 'climb_wall':
|
|
774
|
+
connector.action('climbing_up');
|
|
775
|
+
break;
|
|
776
|
+
|
|
777
|
+
case 'visit_window':
|
|
778
|
+
// 알려진 윈도우 중 랜덤으로 하나 선택 후 타이틀바 위로 점프
|
|
779
|
+
if (knownWindows.length > 0) {
|
|
780
|
+
const win = knownWindows[Math.floor(Math.random() * knownWindows.length)];
|
|
781
|
+
connector.jumpTo(win.x + win.width / 2, win.y);
|
|
782
|
+
if (selected.speech) connector.speak(selected.speech);
|
|
783
|
+
}
|
|
784
|
+
break;
|
|
785
|
+
|
|
786
|
+
case 'return_home':
|
|
787
|
+
if (homePosition) {
|
|
788
|
+
connector.jumpTo(homePosition.x, homePosition.y);
|
|
789
|
+
} else {
|
|
790
|
+
connector.action('idle');
|
|
791
|
+
}
|
|
792
|
+
if (selected.speech) connector.speak(selected.speech);
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// 탐험 기록 저장 (최근 20개)
|
|
797
|
+
explorationHistory.push({ type: selected.type, time: now });
|
|
798
|
+
if (explorationHistory.length > 20) {
|
|
799
|
+
explorationHistory.shift();
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* 윈도우 위치 정보 주기적 갱신 (30초 간격)
|
|
805
|
+
* OS에서 열린 윈도우 목록을 가져와 탐험에 활용
|
|
806
|
+
*/
|
|
807
|
+
function handleWindowCheck(now) {
|
|
808
|
+
const windowCheckInterval = 30000; // 30초
|
|
809
|
+
if (now - lastWindowCheckTime < windowCheckInterval) return;
|
|
810
|
+
lastWindowCheckTime = now;
|
|
811
|
+
connector.queryWindows();
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* 바탕화면 폴더 나르기 (3분 간격, 10% 확률)
|
|
816
|
+
* 바탕화면 폴더를 하나 집어서 잠시 들고 다니다가 내려놓음
|
|
817
|
+
*/
|
|
818
|
+
function handleFolderCarry(now) {
|
|
819
|
+
const carryInterval = 3 * 60 * 1000; // 3분
|
|
820
|
+
if (now - lastFolderCarryTime < carryInterval) return;
|
|
821
|
+
|
|
822
|
+
// 10% 확률
|
|
823
|
+
if (Math.random() > 0.1) return;
|
|
824
|
+
lastFolderCarryTime = now;
|
|
825
|
+
|
|
826
|
+
try {
|
|
827
|
+
const desktopPath = path.join(os.homedir(), 'Desktop');
|
|
828
|
+
if (!fs.existsSync(desktopPath)) return;
|
|
829
|
+
|
|
830
|
+
const entries = fs.readdirSync(desktopPath, { withFileTypes: true });
|
|
831
|
+
// 폴더만 필터 (숨김 폴더 제외, 안전한 것만)
|
|
832
|
+
const folders = entries
|
|
833
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
834
|
+
.map(e => e.name);
|
|
835
|
+
|
|
836
|
+
if (folders.length === 0) return;
|
|
837
|
+
|
|
838
|
+
const folder = folders[Math.floor(Math.random() * folders.length)];
|
|
839
|
+
connector.decide({
|
|
840
|
+
action: 'carrying',
|
|
841
|
+
speech: `${folder} 폴더 들고 다녀볼까~`,
|
|
842
|
+
emotion: 'playful',
|
|
843
|
+
});
|
|
844
|
+
connector.carryFile(folder);
|
|
845
|
+
|
|
846
|
+
// 5초 후 내려놓기
|
|
847
|
+
setTimeout(() => {
|
|
848
|
+
if (connector && connector.connected) {
|
|
849
|
+
connector.dropFile();
|
|
850
|
+
connector.speak('여기 놔둘게~');
|
|
851
|
+
}
|
|
852
|
+
}, 5000);
|
|
853
|
+
} catch {
|
|
854
|
+
// 바탕화면 폴더 접근 실패 — 무시
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// =====================================================
|
|
859
|
+
// npm 패키지 버전 체크 (npm install -g 사용자용)
|
|
860
|
+
// =====================================================
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* npm registry에서 최신 버전을 확인하고,
|
|
864
|
+
* 현재 버전과 다르면 콘솔 + 펫 말풍선으로 알림
|
|
865
|
+
*/
|
|
866
|
+
async function checkNpmUpdate() {
|
|
867
|
+
try {
|
|
868
|
+
const { execSync } = require('child_process');
|
|
869
|
+
const latest = execSync('npm view clawmate version', {
|
|
870
|
+
encoding: 'utf-8',
|
|
871
|
+
timeout: 10000,
|
|
872
|
+
}).trim();
|
|
873
|
+
const current = require('./package.json').version;
|
|
874
|
+
|
|
875
|
+
if (latest !== current) {
|
|
876
|
+
console.log(`[ClawMate] 새 버전 ${latest} 사용 가능 (현재: ${current})`);
|
|
877
|
+
console.log('[ClawMate] 업데이트: npm update -g clawmate');
|
|
878
|
+
if (connector && connector.connected) {
|
|
879
|
+
connector.speak(`업데이트가 있어! v${latest}`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
} catch {
|
|
883
|
+
// npm registry 접근 실패 — 무시 (오프라인 등)
|
|
884
|
+
}
|
|
885
|
+
}
|
package/main/ai-bridge.js
CHANGED
|
@@ -152,6 +152,42 @@ class AIBridge extends EventEmitter {
|
|
|
152
152
|
this.emit('accessorize', payload);
|
|
153
153
|
break;
|
|
154
154
|
|
|
155
|
+
// === 공간 이동 명령 ===
|
|
156
|
+
case 'jump_to':
|
|
157
|
+
// 특정 위치로 점프
|
|
158
|
+
// payload: { x, y }
|
|
159
|
+
this.emit('jump_to', payload);
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case 'rappel':
|
|
163
|
+
// 레펠 (천장/벽에서 실 타고 내려가기)
|
|
164
|
+
// payload: {}
|
|
165
|
+
this.emit('rappel', payload);
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
case 'release_thread':
|
|
169
|
+
// 레펠 실 해제 (낙하)
|
|
170
|
+
// payload: {}
|
|
171
|
+
this.emit('release_thread', payload);
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
case 'move_to_center':
|
|
175
|
+
// 화면 중앙으로 이동
|
|
176
|
+
// payload: {}
|
|
177
|
+
this.emit('move_to_center', payload);
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case 'walk_on_window':
|
|
181
|
+
// 특정 윈도우 타이틀바 위로 이동
|
|
182
|
+
// payload: { windowId, x, y }
|
|
183
|
+
this.emit('walk_on_window', payload);
|
|
184
|
+
break;
|
|
185
|
+
|
|
186
|
+
case 'query_windows':
|
|
187
|
+
// 윈도우 위치 정보 요청 → main process에서 처리
|
|
188
|
+
this.emit('query_windows', payload);
|
|
189
|
+
break;
|
|
190
|
+
|
|
155
191
|
// === 컨텍스트 질의 ===
|
|
156
192
|
case 'query_state':
|
|
157
193
|
// 현재 펫 상태 요청
|
|
@@ -240,6 +276,15 @@ class AIBridge extends EventEmitter {
|
|
|
240
276
|
});
|
|
241
277
|
}
|
|
242
278
|
|
|
279
|
+
reportScreenCapture(imageBase64, width, height) {
|
|
280
|
+
this.send('screen_capture', {
|
|
281
|
+
image: imageBase64,
|
|
282
|
+
width,
|
|
283
|
+
height,
|
|
284
|
+
timestamp: Date.now(),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
243
288
|
// === 상태 업데이트 ===
|
|
244
289
|
|
|
245
290
|
updatePetState(updates) {
|