clawmate 1.3.0 → 1.4.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 +1 -1
- package/index.js +198 -14
- package/main/ai-bridge.js +24 -18
- package/main/ai-connector.js +17 -12
- package/main/autostart.js +3 -3
- package/main/file-command-parser.js +40 -4
- package/main/index.js +7 -5
- package/main/ipc-handlers.js +6 -3
- package/main/telegram.js +36 -3
- package/main/tray.js +214 -59
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -4
- package/preload/preload.js +4 -4
- package/renderer/first-run.html +2 -2
- package/renderer/js/ai-controller.js +20 -9
- package/renderer/js/app.js +9 -6
- package/renderer/js/browser-watcher.js +1 -1
- package/renderer/js/interactions.js +45 -2
- package/renderer/js/memory.js +108 -1
- package/renderer/js/metrics.js +2 -2
- package/renderer/js/mode-manager.js +53 -9
- package/renderer/launcher.html +3 -3
- package/shared/personalities.js +37 -2
- package/skills/launch-pet/index.js +1 -1
- package/skills/launch-pet/skill.json +1 -1
package/electron-builder.yml
CHANGED
package/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ClawMate
|
|
2
|
+
* ClawMate 플러그인 진입점
|
|
3
3
|
*
|
|
4
|
-
* 핵심 원칙:
|
|
4
|
+
* 핵심 원칙: AI가 연결되면 자동으로 ClawMate를 찾아서 연결.
|
|
5
5
|
*
|
|
6
6
|
* 흐름:
|
|
7
|
-
*
|
|
7
|
+
* 플러그인 로드 → init() 자동 호출
|
|
8
8
|
* → ClawMate 실행 중인지 확인 (ws://127.0.0.1:9320 연결 시도)
|
|
9
9
|
* → 실행 중이면: 바로 연결, AI 뇌 역할 시작
|
|
10
10
|
* → 안 돌고 있으면: Electron 앱 자동 실행 → 연결
|
|
@@ -14,7 +14,7 @@ const { spawn } = require('child_process');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const os = require('os');
|
|
16
16
|
const fs = require('fs');
|
|
17
|
-
const {
|
|
17
|
+
const { ClawMateConnector } = require('./main/ai-connector');
|
|
18
18
|
|
|
19
19
|
let connector = null;
|
|
20
20
|
let electronProcess = null;
|
|
@@ -61,14 +61,18 @@ let behaviorAdjustments = { // 현재 적용 중인 행동 조정값
|
|
|
61
61
|
};
|
|
62
62
|
let lastMetricsLogTime = 0; // 마지막 품질 보고서 로그 시각
|
|
63
63
|
|
|
64
|
+
// AI 모션 생성 시스템 상태
|
|
65
|
+
let lastMotionGenTime = 0; // 마지막 모션 생성 시각
|
|
66
|
+
let generatedMotionCount = 0; // 생성된 모션 수
|
|
67
|
+
|
|
64
68
|
module.exports = {
|
|
65
69
|
id: 'clawmate',
|
|
66
70
|
name: 'ClawMate',
|
|
67
|
-
version: '1.
|
|
68
|
-
description: '
|
|
71
|
+
version: '1.4.0',
|
|
72
|
+
description: 'ClawMate 데스크톱 펫 - AI가 조종하는 살아있는 펫',
|
|
69
73
|
|
|
70
74
|
/**
|
|
71
|
-
*
|
|
75
|
+
* 플러그인 로드 시 자동 호출
|
|
72
76
|
* → ClawMate 자동 실행 + 자동 연결
|
|
73
77
|
*/
|
|
74
78
|
async init(api) {
|
|
@@ -170,7 +174,7 @@ module.exports = {
|
|
|
170
174
|
},
|
|
171
175
|
|
|
172
176
|
/**
|
|
173
|
-
*
|
|
177
|
+
* 플러그인 종료 시 정리
|
|
174
178
|
*/
|
|
175
179
|
async destroy() {
|
|
176
180
|
console.log('[ClawMate] 플러그인 정리');
|
|
@@ -188,7 +192,7 @@ module.exports = {
|
|
|
188
192
|
// =====================================================
|
|
189
193
|
|
|
190
194
|
/**
|
|
191
|
-
*
|
|
195
|
+
* 플러그인 시작 시 자동으로 ClawMate 찾기/실행/연결
|
|
192
196
|
* 무한 재시도 — ClawMate가 살아있는 한 항상 연결 유지
|
|
193
197
|
*/
|
|
194
198
|
async function autoConnect() {
|
|
@@ -214,7 +218,7 @@ async function autoConnect() {
|
|
|
214
218
|
function tryConnect() {
|
|
215
219
|
return new Promise((resolve) => {
|
|
216
220
|
if (!connector) {
|
|
217
|
-
connector = new
|
|
221
|
+
connector = new ClawMateConnector(9320);
|
|
218
222
|
setupConnectorEvents();
|
|
219
223
|
}
|
|
220
224
|
|
|
@@ -308,7 +312,7 @@ function setupConnectorEvents() {
|
|
|
308
312
|
*/
|
|
309
313
|
function onConnected() {
|
|
310
314
|
if (connector && connector.connected) {
|
|
311
|
-
connector.speak('
|
|
315
|
+
connector.speak('AI 연결됨! 같이 놀자!');
|
|
312
316
|
connector.action('excited');
|
|
313
317
|
|
|
314
318
|
// "집" 위치 설정 — 화면 하단 왼쪽을 기본 홈으로
|
|
@@ -387,6 +391,14 @@ async function handleUserEvent(event) {
|
|
|
387
391
|
}
|
|
388
392
|
break;
|
|
389
393
|
|
|
394
|
+
case 'double_click':
|
|
395
|
+
connector.decide({
|
|
396
|
+
action: 'excited',
|
|
397
|
+
speech: '우와! 더블클릭이다! 기분 좋아~',
|
|
398
|
+
emotion: 'happy',
|
|
399
|
+
});
|
|
400
|
+
break;
|
|
401
|
+
|
|
390
402
|
case 'drag':
|
|
391
403
|
connector.speak('으앗, 나를 옮기다니!');
|
|
392
404
|
break;
|
|
@@ -1076,6 +1088,9 @@ async function thinkCycle() {
|
|
|
1076
1088
|
|
|
1077
1089
|
// --- 9) 바탕화면 폴더 나르기 (3분 간격, 10% 확률) ---
|
|
1078
1090
|
handleFolderCarry(now);
|
|
1091
|
+
|
|
1092
|
+
// --- 10) AI 모션 생성 (2분 간격, 15% 확률) ---
|
|
1093
|
+
handleMotionGeneration(now, state);
|
|
1079
1094
|
}
|
|
1080
1095
|
|
|
1081
1096
|
/**
|
|
@@ -1253,7 +1268,7 @@ function handleDesktopCheck(now) {
|
|
|
1253
1268
|
|
|
1254
1269
|
/**
|
|
1255
1270
|
* 화면 관찰 (2분 간격, 10% 확률)
|
|
1256
|
-
* 스크린샷을 캡처해서
|
|
1271
|
+
* 스크린샷을 캡처해서 AI가 화면 내용을 인식
|
|
1257
1272
|
*/
|
|
1258
1273
|
function handleScreenObservation(now) {
|
|
1259
1274
|
const screenCheckInterval = 2 * 60 * 1000; // 2분
|
|
@@ -1286,7 +1301,7 @@ function weightedRandom(items) {
|
|
|
1286
1301
|
}
|
|
1287
1302
|
|
|
1288
1303
|
// =====================================================
|
|
1289
|
-
// 공간 탐험 시스템 —
|
|
1304
|
+
// 공간 탐험 시스템 — 펫이 컴퓨터를 "집"처럼 돌아다님
|
|
1290
1305
|
// =====================================================
|
|
1291
1306
|
|
|
1292
1307
|
/**
|
|
@@ -1410,6 +1425,175 @@ function handleFolderCarry(now) {
|
|
|
1410
1425
|
}
|
|
1411
1426
|
}
|
|
1412
1427
|
|
|
1428
|
+
// =====================================================
|
|
1429
|
+
// AI 모션 생성 시스템 — 키프레임 기반 움직임을 동적 생성
|
|
1430
|
+
// =====================================================
|
|
1431
|
+
|
|
1432
|
+
/**
|
|
1433
|
+
* AI 모션 생성 처리 (2분 간격, 15% 확률)
|
|
1434
|
+
* 상황에 맞는 커스텀 이동 패턴을 AI가 직접 생성하여 등록+실행
|
|
1435
|
+
*
|
|
1436
|
+
* 생성 전략:
|
|
1437
|
+
* 1차: apiRef.generate()로 완전한 키프레임 데이터 생성
|
|
1438
|
+
* 2차: 상태 기반 프로시저럴 모션 생성 (폴백)
|
|
1439
|
+
*/
|
|
1440
|
+
async function handleMotionGeneration(now, state) {
|
|
1441
|
+
const motionGenInterval = 2 * 60 * 1000; // 2분
|
|
1442
|
+
if (now - lastMotionGenTime < motionGenInterval) return;
|
|
1443
|
+
if (Math.random() > 0.15) return; // 15% 확률
|
|
1444
|
+
lastMotionGenTime = now;
|
|
1445
|
+
|
|
1446
|
+
const currentState = state?.action || state?.state || 'idle';
|
|
1447
|
+
|
|
1448
|
+
// AI로 모션 생성 시도
|
|
1449
|
+
let motionDef = null;
|
|
1450
|
+
if (apiRef?.generate) {
|
|
1451
|
+
try {
|
|
1452
|
+
motionDef = await generateMotionWithAI(currentState);
|
|
1453
|
+
} catch {}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// 폴백: 프로시저럴 모션 생성
|
|
1457
|
+
if (!motionDef) {
|
|
1458
|
+
motionDef = generateProceduralMotion(currentState, now);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
if (motionDef && connector?.connected) {
|
|
1462
|
+
const motionName = `ai_motion_${generatedMotionCount++}`;
|
|
1463
|
+
connector.registerMovement(motionName, motionDef);
|
|
1464
|
+
|
|
1465
|
+
// 잠시 후 실행
|
|
1466
|
+
setTimeout(() => {
|
|
1467
|
+
if (connector?.connected) {
|
|
1468
|
+
connector.customMove(motionName, {});
|
|
1469
|
+
console.log(`[ClawMate] AI 모션 생성 실행: ${motionName}`);
|
|
1470
|
+
}
|
|
1471
|
+
}, 500);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* AI로 키프레임 모션 생성
|
|
1477
|
+
* 수학 공식(formula) 또는 웨이포인트(waypoints) 방식의 모션 정의를 생성
|
|
1478
|
+
*/
|
|
1479
|
+
async function generateMotionWithAI(currentState) {
|
|
1480
|
+
const prompt = `현재 펫 상태: ${currentState}.
|
|
1481
|
+
이 상황에 어울리는 재미있는 이동 패턴을 JSON으로 만들어줘.
|
|
1482
|
+
|
|
1483
|
+
두 가지 형식 중 하나를 선택:
|
|
1484
|
+
1) formula 방식 (수학적 궤도):
|
|
1485
|
+
{"type":"formula","formula":{"xAmp":80,"yAmp":40,"xFreq":1,"yFreq":2,"xPhase":0,"yPhase":0},"duration":3000,"speed":1.5}
|
|
1486
|
+
|
|
1487
|
+
2) waypoints 방식 (경로점):
|
|
1488
|
+
{"type":"waypoints","waypoints":[{"x":100,"y":200,"pause":300},{"x":300,"y":100},{"x":500,"y":250}],"speed":2}
|
|
1489
|
+
|
|
1490
|
+
규칙:
|
|
1491
|
+
- xAmp/yAmp: 10~150 사이 (화면 크기 고려)
|
|
1492
|
+
- duration: 2000~6000ms
|
|
1493
|
+
- waypoints: 3~6개
|
|
1494
|
+
- speed: 0.5~3
|
|
1495
|
+
- 펫 성격에 맞게: 장난스럽고 귀여운 움직임
|
|
1496
|
+
JSON만 출력해.`;
|
|
1497
|
+
|
|
1498
|
+
const response = await apiRef.generate(prompt);
|
|
1499
|
+
if (!response || typeof response !== 'string') return null;
|
|
1500
|
+
|
|
1501
|
+
// JSON 파싱
|
|
1502
|
+
let jsonStr = response;
|
|
1503
|
+
const jsonMatch = response.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
1504
|
+
if (jsonMatch) jsonStr = jsonMatch[1].trim();
|
|
1505
|
+
else {
|
|
1506
|
+
const braceMatch = response.match(/\{[\s\S]*\}/);
|
|
1507
|
+
if (braceMatch) jsonStr = braceMatch[0];
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
try {
|
|
1511
|
+
const def = JSON.parse(jsonStr);
|
|
1512
|
+
// 기본 검증
|
|
1513
|
+
if (def.type === 'formula' && def.formula) {
|
|
1514
|
+
def.duration = Math.min(6000, Math.max(2000, def.duration || 3000));
|
|
1515
|
+
return def;
|
|
1516
|
+
}
|
|
1517
|
+
if (def.type === 'waypoints' && Array.isArray(def.waypoints) && def.waypoints.length >= 2) {
|
|
1518
|
+
return def;
|
|
1519
|
+
}
|
|
1520
|
+
} catch {}
|
|
1521
|
+
return null;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* 프로시저럴 모션 생성 (AI 없을 때 폴백)
|
|
1526
|
+
* 현재 상태와 시간에 따라 수학적으로 모션 패턴 생성
|
|
1527
|
+
*/
|
|
1528
|
+
function generateProceduralMotion(currentState, now) {
|
|
1529
|
+
const hour = new Date(now).getHours();
|
|
1530
|
+
const seed = now % 1000;
|
|
1531
|
+
|
|
1532
|
+
// 상태별 모션 특성
|
|
1533
|
+
const stateMotions = {
|
|
1534
|
+
idle: () => {
|
|
1535
|
+
// 가벼운 좌우 흔들림 또는 작은 원
|
|
1536
|
+
if (seed > 500) {
|
|
1537
|
+
return {
|
|
1538
|
+
type: 'formula',
|
|
1539
|
+
formula: { xAmp: 20 + seed % 30, yAmp: 5 + seed % 10, xFreq: 0.5, yFreq: 1, xPhase: 0, yPhase: Math.PI / 2 },
|
|
1540
|
+
duration: 3000,
|
|
1541
|
+
speed: 0.8,
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
return {
|
|
1545
|
+
type: 'formula',
|
|
1546
|
+
formula: { xAmp: 15, yAmp: 15, xFreq: 1, yFreq: 1, xPhase: 0, yPhase: Math.PI / 2 },
|
|
1547
|
+
duration: 2500,
|
|
1548
|
+
speed: 0.6,
|
|
1549
|
+
};
|
|
1550
|
+
},
|
|
1551
|
+
walking: () => {
|
|
1552
|
+
// 지그재그 또는 사인파 이동
|
|
1553
|
+
const amp = 30 + seed % 50;
|
|
1554
|
+
return {
|
|
1555
|
+
type: 'formula',
|
|
1556
|
+
formula: { xAmp: amp, yAmp: amp * 0.3, xFreq: 0.5, yFreq: 2, xPhase: 0, yPhase: 0 },
|
|
1557
|
+
duration: 4000,
|
|
1558
|
+
speed: 1.2,
|
|
1559
|
+
};
|
|
1560
|
+
},
|
|
1561
|
+
excited: () => {
|
|
1562
|
+
// 활발한 8자 궤도
|
|
1563
|
+
return {
|
|
1564
|
+
type: 'formula',
|
|
1565
|
+
formula: { xAmp: 80 + seed % 40, yAmp: 40 + seed % 20, xFreq: 1, yFreq: 2, xPhase: 0, yPhase: 0 },
|
|
1566
|
+
duration: 3000,
|
|
1567
|
+
speed: 2.0,
|
|
1568
|
+
};
|
|
1569
|
+
},
|
|
1570
|
+
playing: () => {
|
|
1571
|
+
// 불규칙한 웨이포인트 (놀기 느낌)
|
|
1572
|
+
const points = [];
|
|
1573
|
+
for (let i = 0; i < 4; i++) {
|
|
1574
|
+
points.push({
|
|
1575
|
+
x: 100 + Math.floor(Math.random() * 800),
|
|
1576
|
+
y: 100 + Math.floor(Math.random() * 400),
|
|
1577
|
+
pause: i === 0 ? 200 : 0,
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
return { type: 'waypoints', waypoints: points, speed: 2.5 };
|
|
1581
|
+
},
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1584
|
+
// 야간에는 느린 모션
|
|
1585
|
+
const isNight = hour >= 23 || hour < 6;
|
|
1586
|
+
const generator = stateMotions[currentState] || stateMotions.idle;
|
|
1587
|
+
const motion = generator();
|
|
1588
|
+
|
|
1589
|
+
if (isNight) {
|
|
1590
|
+
motion.speed = Math.min(0.5, (motion.speed || 1) * 0.4);
|
|
1591
|
+
if (motion.duration) motion.duration *= 1.5;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
return motion;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1413
1597
|
// =====================================================
|
|
1414
1598
|
// 자기 관찰 시스템 (Metrics → 행동 조정)
|
|
1415
1599
|
// =====================================================
|
|
@@ -1586,7 +1770,7 @@ function adjustBehavior(metrics) {
|
|
|
1586
1770
|
|
|
1587
1771
|
/**
|
|
1588
1772
|
* 품질 보고서 콘솔 출력 (5분마다)
|
|
1589
|
-
*
|
|
1773
|
+
* 개발자/운영자가 펫의 동작 품질을 모니터링할 수 있도록 한다.
|
|
1590
1774
|
*/
|
|
1591
1775
|
function _logQualityReport(metrics) {
|
|
1592
1776
|
const adj = behaviorAdjustments;
|
package/main/ai-bridge.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* AI ↔ ClawMate 브릿지
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* - ClawMate →
|
|
4
|
+
* AI 에이전트가 ClawMate의 뇌 역할을 한다.
|
|
5
|
+
* - AI → ClawMate: 행동 명령, 말풍선, 감정, 이동
|
|
6
|
+
* - ClawMate → AI: 사용자 이벤트 (클릭, 드래그, 커서, 파일 변화)
|
|
7
7
|
*
|
|
8
8
|
* 통신: WebSocket (로컬 ws://localhost:9320)
|
|
9
9
|
* 프로토콜: JSON 메시지
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* AI 연결 안 됐을 때 → 자율 모드 (기존 FSM) 로 폴백
|
|
12
12
|
*/
|
|
13
13
|
const WebSocket = require('ws');
|
|
14
14
|
const EventEmitter = require('events');
|
|
@@ -17,7 +17,7 @@ class AIBridge extends EventEmitter {
|
|
|
17
17
|
constructor() {
|
|
18
18
|
super();
|
|
19
19
|
this.wss = null;
|
|
20
|
-
this.client = null; // 연결된
|
|
20
|
+
this.client = null; // 연결된 AI 에이전트
|
|
21
21
|
this.connected = false;
|
|
22
22
|
this.port = 9320;
|
|
23
23
|
this.heartbeatInterval = null;
|
|
@@ -33,19 +33,19 @@ class AIBridge extends EventEmitter {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* WebSocket 서버 시작 —
|
|
36
|
+
* WebSocket 서버 시작 — AI 에이전트가 여기에 접속
|
|
37
37
|
*/
|
|
38
38
|
start() {
|
|
39
39
|
this.wss = new WebSocket.Server({ port: this.port, host: '127.0.0.1' });
|
|
40
40
|
|
|
41
41
|
this.wss.on('connection', (ws) => {
|
|
42
|
-
console.log('[AI Bridge]
|
|
42
|
+
console.log('[AI Bridge] AI 연결됨');
|
|
43
43
|
this.client = ws;
|
|
44
44
|
this.connected = true;
|
|
45
45
|
this.reconnectAttempts = 0;
|
|
46
46
|
this.emit('connected');
|
|
47
47
|
|
|
48
|
-
//
|
|
48
|
+
// AI에 현재 상태 전송
|
|
49
49
|
this.send('sync', this.petState);
|
|
50
50
|
|
|
51
51
|
ws.on('message', (data) => {
|
|
@@ -58,7 +58,7 @@ class AIBridge extends EventEmitter {
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
ws.on('close', () => {
|
|
61
|
-
console.log('[AI Bridge]
|
|
61
|
+
console.log('[AI Bridge] AI 연결 해제');
|
|
62
62
|
this.client = null;
|
|
63
63
|
this.connected = false;
|
|
64
64
|
this.emit('disconnected');
|
|
@@ -84,7 +84,7 @@ class AIBridge extends EventEmitter {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
87
|
+
* AI에서 온 명령 처리
|
|
88
88
|
*/
|
|
89
89
|
_handleCommand(msg) {
|
|
90
90
|
const { type, payload } = msg;
|
|
@@ -92,7 +92,7 @@ class AIBridge extends EventEmitter {
|
|
|
92
92
|
switch (type) {
|
|
93
93
|
// === 행동 제어 ===
|
|
94
94
|
case 'action':
|
|
95
|
-
//
|
|
95
|
+
// AI가 펫의 행동을 직접 지시
|
|
96
96
|
// payload: { state: 'walking'|'excited'|..., duration?: ms }
|
|
97
97
|
this.emit('action', payload);
|
|
98
98
|
break;
|
|
@@ -111,7 +111,7 @@ class AIBridge extends EventEmitter {
|
|
|
111
111
|
|
|
112
112
|
// === 말하기 ===
|
|
113
113
|
case 'speak':
|
|
114
|
-
//
|
|
114
|
+
// AI가 펫을 통해 사용자에게 말함
|
|
115
115
|
// payload: { text: string, style?: 'normal'|'thought'|'shout' }
|
|
116
116
|
this.emit('speak', payload);
|
|
117
117
|
break;
|
|
@@ -196,7 +196,7 @@ class AIBridge extends EventEmitter {
|
|
|
196
196
|
|
|
197
197
|
// === 커스텀 이동 패턴 ===
|
|
198
198
|
case 'register_movement':
|
|
199
|
-
//
|
|
199
|
+
// AI가 커스텀 이동 패턴 등록
|
|
200
200
|
// payload: { name: string, definition: { type: 'waypoints'|'formula'|'sequence', ... } }
|
|
201
201
|
this.emit('register_movement', payload);
|
|
202
202
|
break;
|
|
@@ -231,6 +231,12 @@ class AIBridge extends EventEmitter {
|
|
|
231
231
|
this.emit('reset_character', payload);
|
|
232
232
|
break;
|
|
233
233
|
|
|
234
|
+
case 'set_persona':
|
|
235
|
+
// 봇 인격체 전환 (Incarnation 모드)
|
|
236
|
+
// payload: { name, personality, speakingStyle, color?, ... }
|
|
237
|
+
this.emit('set_persona', payload);
|
|
238
|
+
break;
|
|
239
|
+
|
|
234
240
|
// === 컨텍스트 질의 ===
|
|
235
241
|
case 'query_state':
|
|
236
242
|
// 현재 펫 상태 요청
|
|
@@ -244,7 +250,7 @@ class AIBridge extends EventEmitter {
|
|
|
244
250
|
|
|
245
251
|
// === AI 의사결정 결과 ===
|
|
246
252
|
case 'ai_decision':
|
|
247
|
-
//
|
|
253
|
+
// AI의 종합적 의사결정
|
|
248
254
|
// payload: { action, speech?, emotion?, reasoning? }
|
|
249
255
|
this.emit('ai_decision', payload);
|
|
250
256
|
break;
|
|
@@ -255,7 +261,7 @@ class AIBridge extends EventEmitter {
|
|
|
255
261
|
}
|
|
256
262
|
|
|
257
263
|
/**
|
|
258
|
-
*
|
|
264
|
+
* AI에 이벤트 전송
|
|
259
265
|
*/
|
|
260
266
|
send(type, payload) {
|
|
261
267
|
if (!this.client || this.client.readyState !== WebSocket.OPEN) return false;
|
|
@@ -267,7 +273,7 @@ class AIBridge extends EventEmitter {
|
|
|
267
273
|
}
|
|
268
274
|
}
|
|
269
275
|
|
|
270
|
-
// === 사용자 이벤트 리포트 (ClawMate →
|
|
276
|
+
// === 사용자 이벤트 리포트 (ClawMate → AI) ===
|
|
271
277
|
|
|
272
278
|
reportUserClick(position) {
|
|
273
279
|
this.send('user_event', {
|
|
@@ -329,7 +335,7 @@ class AIBridge extends EventEmitter {
|
|
|
329
335
|
}
|
|
330
336
|
|
|
331
337
|
/**
|
|
332
|
-
* 메트릭 데이터를
|
|
338
|
+
* 메트릭 데이터를 AI에 전송
|
|
333
339
|
* 렌더러에서 수집한 펫 동작 품질 메트릭을 AI에 전달
|
|
334
340
|
*/
|
|
335
341
|
reportMetrics(summary) {
|
package/main/ai-connector.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* AI 에이전트 측 커넥터
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* AI가 ClawMate에 접속해서 펫을 조종하는 클라이언트.
|
|
5
|
+
* ClawMate 플러그인(index.js)에서 사용됨.
|
|
6
6
|
*
|
|
7
7
|
* 사용 예:
|
|
8
|
-
* const connector = new
|
|
8
|
+
* const connector = new ClawMateConnector();
|
|
9
9
|
* await connector.connect();
|
|
10
10
|
* connector.speak('안녕! 오늘 뭐 할 거야?');
|
|
11
11
|
* connector.action('excited');
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
const WebSocket = require('ws');
|
|
15
15
|
const EventEmitter = require('events');
|
|
16
16
|
|
|
17
|
-
class
|
|
17
|
+
class ClawMateConnector extends EventEmitter {
|
|
18
18
|
constructor(port = 9320) {
|
|
19
19
|
super();
|
|
20
20
|
this.port = port;
|
|
@@ -81,12 +81,12 @@ class OpenClawConnector extends EventEmitter {
|
|
|
81
81
|
break;
|
|
82
82
|
|
|
83
83
|
case 'user_event':
|
|
84
|
-
// 사용자 이벤트 →
|
|
84
|
+
// 사용자 이벤트 → AI가 반응 결정
|
|
85
85
|
this.emit('user_event', payload);
|
|
86
86
|
break;
|
|
87
87
|
|
|
88
88
|
case 'screen_capture':
|
|
89
|
-
// 화면 캡처 응답 →
|
|
89
|
+
// 화면 캡처 응답 → AI가 분석
|
|
90
90
|
this.emit('screen_capture', payload);
|
|
91
91
|
break;
|
|
92
92
|
|
|
@@ -96,7 +96,7 @@ class OpenClawConnector extends EventEmitter {
|
|
|
96
96
|
break;
|
|
97
97
|
|
|
98
98
|
case 'metrics_report':
|
|
99
|
-
// 메트릭 데이터 수신 →
|
|
99
|
+
// 메트릭 데이터 수신 → AI가 분석
|
|
100
100
|
this.emit('metrics_report', payload);
|
|
101
101
|
break;
|
|
102
102
|
|
|
@@ -115,7 +115,7 @@ class OpenClawConnector extends EventEmitter {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
// ===
|
|
118
|
+
// === AI → ClawMate 명령 API ===
|
|
119
119
|
|
|
120
120
|
/** 펫이 말하게 함 */
|
|
121
121
|
speak(text, style = 'normal') {
|
|
@@ -164,13 +164,13 @@ class OpenClawConnector extends EventEmitter {
|
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
166
|
* AI 종합 의사결정 전송
|
|
167
|
-
*
|
|
167
|
+
* AI가 상황을 분석하고 내린 결정을 한번에 전송
|
|
168
168
|
*/
|
|
169
169
|
decide(decision) {
|
|
170
170
|
return this._send('ai_decision', decision);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
// === 공간 이동 API (
|
|
173
|
+
// === 공간 이동 API (펫이 컴퓨터를 "집"처럼 돌아다님) ===
|
|
174
174
|
|
|
175
175
|
/** 특정 위치로 점프 */
|
|
176
176
|
jumpTo(x, y) {
|
|
@@ -270,6 +270,11 @@ class OpenClawConnector extends EventEmitter {
|
|
|
270
270
|
return this._send('reset_character', {});
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/** 인격체 전환 (Incarnation 모드에서 봇 인격 반영) */
|
|
274
|
+
setPersona(data) {
|
|
275
|
+
return this._send('set_persona', data);
|
|
276
|
+
}
|
|
277
|
+
|
|
273
278
|
/**
|
|
274
279
|
* 현재 펫 상태 요청 (Promise 반환)
|
|
275
280
|
* 서버에서 state_response가 오면 resolve, 타임아웃 시 캐시된 상태 반환
|
|
@@ -319,4 +324,4 @@ class OpenClawConnector extends EventEmitter {
|
|
|
319
324
|
}
|
|
320
325
|
}
|
|
321
326
|
|
|
322
|
-
module.exports = {
|
|
327
|
+
module.exports = { ClawMateConnector };
|
package/main/autostart.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* macOS: LaunchAgent plist
|
|
6
6
|
* Linux: ~/.config/autostart/ 디렉토리에 .desktop 파일 생성
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* ClawMate가 먼저 혼자 자율 모드로 돌다가 →
|
|
8
|
+
* AI가 나중에 켜져도, ClawMate는 이미 돌고 있어서 바로 연결됨.
|
|
9
|
+
* ClawMate가 먼저 혼자 자율 모드로 돌다가 → AI 연결되면 AI 모드 전환.
|
|
10
10
|
*/
|
|
11
11
|
const { app } = require('electron');
|
|
12
12
|
const fs = require('fs');
|
|
@@ -22,7 +22,7 @@ function getDesktopFileContent() {
|
|
|
22
22
|
'[Desktop Entry]',
|
|
23
23
|
'Type=Application',
|
|
24
24
|
'Name=ClawMate',
|
|
25
|
-
'Comment=
|
|
25
|
+
'Comment=ClawMate Desktop Pet',
|
|
26
26
|
`Exec=${process.execPath} ${path.resolve(__dirname, '..')}`,
|
|
27
27
|
'X-GNOME-Autostart-enabled=true',
|
|
28
28
|
'Hidden=false',
|
|
@@ -297,20 +297,55 @@ function parseCharacterCommand(text) {
|
|
|
297
297
|
* @param {string} text - 사용자 메시지
|
|
298
298
|
* @returns {{ type: string, ... }}
|
|
299
299
|
*/
|
|
300
|
+
/**
|
|
301
|
+
* 모드/설정 변경 명령 감지
|
|
302
|
+
* @param {string} text - 사용자 메시지
|
|
303
|
+
* @returns {{ type: 'mode_change', mode: string }|{ type: 'setting', key, value }|null}
|
|
304
|
+
*/
|
|
305
|
+
function parseSettingCommand(text) {
|
|
306
|
+
if (!text) return null;
|
|
307
|
+
const t = text.trim().toLowerCase();
|
|
308
|
+
|
|
309
|
+
// 모드 변경
|
|
310
|
+
if (/(?:펫|pet)\s*모드/.test(t)) return { type: 'mode_change', mode: 'pet' };
|
|
311
|
+
if (/(?:인카|인격|incarnation|claw)\s*모드/.test(t)) return { type: 'mode_change', mode: 'incarnation' };
|
|
312
|
+
if (/둘\s*다\s*모드|both\s*mode/i.test(t)) return { type: 'mode_change', mode: 'both' };
|
|
313
|
+
|
|
314
|
+
// 캐릭터 프리셋 선택 (트레이 프리셋과 동일)
|
|
315
|
+
const presetMap = {
|
|
316
|
+
'파란|파랑|blue': 'blue', '초록|green': 'green', '보라|purple': 'purple',
|
|
317
|
+
'골드|금색|gold': 'gold', '핑크|pink': 'pink',
|
|
318
|
+
'고양이|cat': 'cat', '로봇|robot': 'robot', '유령|ghost': 'ghost', '드래곤|dragon': 'dragon',
|
|
319
|
+
'기본|default|원래': 'default',
|
|
320
|
+
};
|
|
321
|
+
for (const [pattern, preset] of Object.entries(presetMap)) {
|
|
322
|
+
const regex = new RegExp(`(?:${pattern})\\s*(?:캐릭터|색|색상|으로)?\\s*(?:바꿔|변경|골라|선택|해줘)?`);
|
|
323
|
+
if (regex.test(t)) {
|
|
324
|
+
return { type: 'preset_character', preset };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
300
331
|
function parseMessage(text) {
|
|
301
|
-
// 0순위:
|
|
332
|
+
// 0순위: 설정/모드 변경 명령
|
|
333
|
+
const settingCmd = parseSettingCommand(text);
|
|
334
|
+
if (settingCmd) return settingCmd;
|
|
335
|
+
|
|
336
|
+
// 1순위: 캐릭터 변경 명령 (AI 생성)
|
|
302
337
|
const charCmd = parseCharacterCommand(text);
|
|
303
338
|
if (charCmd) return charCmd;
|
|
304
339
|
|
|
305
|
-
//
|
|
340
|
+
// 2순위: 파일 조작 명령
|
|
306
341
|
const fileCmd = parseFileCommand(text);
|
|
307
342
|
if (fileCmd) return fileCmd;
|
|
308
343
|
|
|
309
|
-
//
|
|
344
|
+
// 3순위: 행동 명령
|
|
310
345
|
const actionCmd = parseActionCommand(text);
|
|
311
346
|
if (actionCmd) return actionCmd;
|
|
312
347
|
|
|
313
|
-
//
|
|
348
|
+
// 4순위: 일반 대화 (speak)
|
|
314
349
|
return { type: 'speak', text };
|
|
315
350
|
}
|
|
316
351
|
|
|
@@ -319,6 +354,7 @@ module.exports = {
|
|
|
319
354
|
parseFileCommand,
|
|
320
355
|
parseActionCommand,
|
|
321
356
|
parseCharacterCommand,
|
|
357
|
+
parseSettingCommand,
|
|
322
358
|
resolveSource,
|
|
323
359
|
AUTO_CATEGORIES,
|
|
324
360
|
};
|
package/main/index.js
CHANGED
|
@@ -69,18 +69,18 @@ function createLauncherWindow() {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* AI Bridge 시작 —
|
|
72
|
+
* AI Bridge 시작 — AI 에이전트가 접속하면 펫을 조종
|
|
73
73
|
*/
|
|
74
74
|
function startAIBridge(win) {
|
|
75
75
|
aiBridge = new AIBridge();
|
|
76
76
|
aiBridge.start();
|
|
77
77
|
|
|
78
|
-
//
|
|
78
|
+
// AI → ClawMate 명령을 렌더러에 전달
|
|
79
79
|
const commandTypes = [
|
|
80
80
|
'action', 'move', 'emote', 'speak', 'think',
|
|
81
81
|
'carry_file', 'drop_file', 'set_mode', 'evolve',
|
|
82
82
|
'accessorize', 'ai_decision',
|
|
83
|
-
// 공간 이동 명령 (
|
|
83
|
+
// 공간 이동 명령 (펫이 집처럼 돌아다니기)
|
|
84
84
|
'jump_to', 'rappel', 'release_thread', 'move_to_center', 'walk_on_window',
|
|
85
85
|
// 커스텀 이동 패턴
|
|
86
86
|
'register_movement', 'custom_move', 'stop_custom_move', 'list_movements',
|
|
@@ -88,6 +88,8 @@ function startAIBridge(win) {
|
|
|
88
88
|
'smart_file_op',
|
|
89
89
|
// 캐릭터 커스터마이징 (텔레그램에서 AI 생성)
|
|
90
90
|
'set_character', 'reset_character',
|
|
91
|
+
// 인격체 전환 (Incarnation 모드)
|
|
92
|
+
'set_persona',
|
|
91
93
|
];
|
|
92
94
|
|
|
93
95
|
commandTypes.forEach((type) => {
|
|
@@ -98,7 +100,7 @@ function startAIBridge(win) {
|
|
|
98
100
|
});
|
|
99
101
|
});
|
|
100
102
|
|
|
101
|
-
//
|
|
103
|
+
// AI 윈도우 위치 정보 요청 처리
|
|
102
104
|
aiBridge.on('query_windows', async () => {
|
|
103
105
|
try {
|
|
104
106
|
const { getWindowPositions } = require('./platform');
|
|
@@ -110,7 +112,7 @@ function startAIBridge(win) {
|
|
|
110
112
|
}
|
|
111
113
|
});
|
|
112
114
|
|
|
113
|
-
//
|
|
115
|
+
// AI 화면 캡처 요청 처리 (main process에서 직접 캡처)
|
|
114
116
|
aiBridge.on('query_screen', async () => {
|
|
115
117
|
try {
|
|
116
118
|
const primaryDisplay = screen.getPrimaryDisplay();
|