clawmate 1.2.0 → 1.3.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/index.js +749 -5
- package/main/ai-bridge.js +54 -0
- package/main/ai-connector.js +78 -0
- package/main/file-command-parser.js +324 -0
- package/main/index.js +12 -0
- package/main/ipc-handlers.js +102 -0
- package/main/platform.js +48 -1
- package/main/smart-file-ops.js +373 -0
- package/main/telegram.js +560 -0
- package/main/tray.js +132 -19
- package/package.json +2 -1
- package/preload/preload.js +16 -0
- package/renderer/index.html +2 -0
- package/renderer/js/ai-controller.js +294 -0
- package/renderer/js/app.js +10 -0
- package/renderer/js/browser-watcher.js +172 -0
- package/renderer/js/character.js +119 -22
- package/renderer/js/metrics.js +607 -0
- package/renderer/js/pet-engine.js +372 -30
- package/renderer/js/state-machine.js +7 -0
- package/shared/messages.js +110 -0
package/main/tray.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const { Tray, Menu, nativeImage, app } = require('electron');
|
|
1
|
+
const { Tray, Menu, nativeImage, app, shell } = require('electron');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
3
4
|
const Store = require('./store');
|
|
4
5
|
const { undoAllMoves, getFileManifest } = require('./file-ops');
|
|
5
6
|
const { isAutoStartEnabled, toggleAutoStart } = require('./autostart');
|
|
@@ -7,28 +8,76 @@ const { isAutoStartEnabled, toggleAutoStart } = require('./autostart');
|
|
|
7
8
|
let tray = null;
|
|
8
9
|
let aiBridge = null;
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* 16x16 Claw 픽셀아트 아이콘 생성
|
|
13
|
+
* 캐릭터 idle 프레임을 축소한 형태
|
|
14
|
+
*
|
|
15
|
+
* 색상 코드:
|
|
16
|
+
* 0 = 투명
|
|
17
|
+
* 1 = #ff4f40 (빨강)
|
|
18
|
+
* 2 = #ff775f (연빨강)
|
|
19
|
+
* 3 = #3a0a0d (갈색 다리)
|
|
20
|
+
* 4 = #ffffff (눈 흰자)
|
|
21
|
+
* 5 = #000000 (눈동자)
|
|
22
|
+
* 6 = #ff4f40 (집게)
|
|
23
|
+
*/
|
|
24
|
+
const CLAW_ICON = [
|
|
25
|
+
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
|
|
26
|
+
[0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
|
|
27
|
+
[0,6,0,0,6,0,0,0,0,0,0,6,0,0,6,0],
|
|
28
|
+
[0,6,0,0,6,0,0,0,0,0,0,6,0,0,6,0],
|
|
29
|
+
[0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
|
|
30
|
+
[0,0,0,6,1,1,0,0,0,0,1,1,6,0,0,0],
|
|
31
|
+
[0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
|
|
32
|
+
[0,0,0,1,1,4,5,1,1,4,5,1,1,0,0,0],
|
|
33
|
+
[0,0,0,1,1,4,5,1,1,4,5,1,1,0,0,0],
|
|
34
|
+
[0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0],
|
|
35
|
+
[0,0,1,2,1,1,1,1,1,1,1,1,2,1,0,0],
|
|
36
|
+
[0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0],
|
|
37
|
+
[0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
|
|
38
|
+
[0,0,3,3,3,0,3,3,3,3,0,3,3,3,0,0],
|
|
39
|
+
[0,3,3,3,0,0,0,3,3,0,0,0,3,3,3,0],
|
|
40
|
+
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const COLOR_MAP = {
|
|
44
|
+
0: [0, 0, 0, 0], // 투명
|
|
45
|
+
1: [255, 79, 64, 255], // primary 빨강
|
|
46
|
+
2: [255, 119, 95, 255], // secondary 연빨강
|
|
47
|
+
3: [58, 10, 13, 255], // dark 갈색
|
|
48
|
+
4: [255, 255, 255, 255], // eye 흰자
|
|
49
|
+
5: [0, 0, 0, 255], // pupil 눈동자
|
|
50
|
+
6: [255, 79, 64, 255], // claw 집게
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* CLAW_ICON 16x16 배열 → nativeImage 변환
|
|
55
|
+
*/
|
|
56
|
+
function createClawIcon() {
|
|
57
|
+
const size = 16;
|
|
58
|
+
const buffer = Buffer.alloc(size * size * 4);
|
|
59
|
+
|
|
60
|
+
for (let y = 0; y < size; y++) {
|
|
61
|
+
for (let x = 0; x < size; x++) {
|
|
62
|
+
const code = CLAW_ICON[y][x];
|
|
63
|
+
const color = COLOR_MAP[code] || COLOR_MAP[0];
|
|
64
|
+
const offset = (y * size + x) * 4;
|
|
65
|
+
buffer[offset + 0] = color[0]; // R
|
|
66
|
+
buffer[offset + 1] = color[1]; // G
|
|
67
|
+
buffer[offset + 2] = color[2]; // B
|
|
68
|
+
buffer[offset + 3] = color[3]; // A
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return nativeImage.createFromBuffer(buffer, { width: size, height: size });
|
|
73
|
+
}
|
|
74
|
+
|
|
10
75
|
function setupTray(mainWindow, bridge) {
|
|
11
76
|
aiBridge = bridge;
|
|
12
77
|
const store = new Store('clawmate-config', { mode: 'pet' });
|
|
13
78
|
|
|
14
|
-
// 트레이 아이콘 생성
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
const iconPath = path.join(__dirname, '..', 'assets', 'icons', 'tray-pet.png');
|
|
18
|
-
icon = nativeImage.createFromPath(iconPath);
|
|
19
|
-
if (icon.isEmpty()) throw new Error('no icon');
|
|
20
|
-
} catch {
|
|
21
|
-
// 16x16 빨간색 아이콘 폴백
|
|
22
|
-
const size = 16;
|
|
23
|
-
const buffer = Buffer.alloc(size * size * 4);
|
|
24
|
-
for (let i = 0; i < size * size; i++) {
|
|
25
|
-
buffer[i * 4 + 0] = 0xff;
|
|
26
|
-
buffer[i * 4 + 1] = 0x4f;
|
|
27
|
-
buffer[i * 4 + 2] = 0x40;
|
|
28
|
-
buffer[i * 4 + 3] = 0xff;
|
|
29
|
-
}
|
|
30
|
-
icon = nativeImage.createFromBuffer(buffer, { width: size, height: size });
|
|
31
|
-
}
|
|
79
|
+
// Claw 픽셀아트 트레이 아이콘 생성
|
|
80
|
+
const icon = createClawIcon();
|
|
32
81
|
|
|
33
82
|
tray = new Tray(icon);
|
|
34
83
|
tray.setToolTip('ClawMate - 데스크톱 펫');
|
|
@@ -89,6 +138,12 @@ function setupTray(mainWindow, bridge) {
|
|
|
89
138
|
},
|
|
90
139
|
},
|
|
91
140
|
{ type: 'separator' },
|
|
141
|
+
{
|
|
142
|
+
label: '업데이트 확인',
|
|
143
|
+
click: async () => {
|
|
144
|
+
await checkForUpdateManual(mainWindow);
|
|
145
|
+
},
|
|
146
|
+
},
|
|
92
147
|
{
|
|
93
148
|
label: '파일 이동 되돌리기',
|
|
94
149
|
click: async () => {
|
|
@@ -122,4 +177,62 @@ function setupTray(mainWindow, bridge) {
|
|
|
122
177
|
return tray;
|
|
123
178
|
}
|
|
124
179
|
|
|
180
|
+
/**
|
|
181
|
+
* 수동 업데이트 확인 (트레이 메뉴에서 클릭)
|
|
182
|
+
* 빌드된 앱: electron-updater 사용
|
|
183
|
+
* npm 설치: npm registry에서 최신 버전 비교
|
|
184
|
+
*/
|
|
185
|
+
async function checkForUpdateManual(mainWindow) {
|
|
186
|
+
if (app.isPackaged) {
|
|
187
|
+
// electron-updater 기반 업데이트
|
|
188
|
+
try {
|
|
189
|
+
const { autoUpdater } = require('electron-updater');
|
|
190
|
+
autoUpdater.checkForUpdatesAndNotify();
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error('[업데이트] electron-updater 실패:', err.message);
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
// npm 기반 업데이트 확인
|
|
196
|
+
try {
|
|
197
|
+
const latest = execSync('npm view clawmate version', {
|
|
198
|
+
encoding: 'utf-8',
|
|
199
|
+
timeout: 10000,
|
|
200
|
+
}).trim();
|
|
201
|
+
const current = require('../package.json').version;
|
|
202
|
+
|
|
203
|
+
if (latest !== current) {
|
|
204
|
+
// 펫 말풍선으로 알림
|
|
205
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
206
|
+
mainWindow.webContents.send('ai-command', {
|
|
207
|
+
type: 'speak',
|
|
208
|
+
payload: { text: `새 버전 v${latest} 사용 가능! (현재: v${current})` },
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
console.log(`[업데이트] 새 버전 ${latest} 사용 가능 (현재: ${current})`);
|
|
212
|
+
console.log('[업데이트] npm update -g clawmate');
|
|
213
|
+
|
|
214
|
+
// npm 페이지 열기
|
|
215
|
+
shell.openExternal('https://www.npmjs.com/package/clawmate');
|
|
216
|
+
} else {
|
|
217
|
+
// 이미 최신
|
|
218
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
219
|
+
mainWindow.webContents.send('ai-command', {
|
|
220
|
+
type: 'speak',
|
|
221
|
+
payload: { text: `v${current} — 이미 최신 버전이야!` },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
console.log(`[업데이트] 현재 최신 버전 (v${current})`);
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error('[업데이트] npm 버전 확인 실패:', err.message);
|
|
228
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
229
|
+
mainWindow.webContents.send('ai-command', {
|
|
230
|
+
type: 'speak',
|
|
231
|
+
payload: { text: '업데이트 확인 실패... 인터넷 연결 확인해봐!' },
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
125
238
|
module.exports = { setupTray };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawmate",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "OpenClaw 데스크톱 펫 - AI가 조종하는 화면 위의 살아있는 Claw",
|
|
5
5
|
"main": "main/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"homepage": "https://github.com/boqum/clawmate",
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"electron-updater": "^6.0.0",
|
|
32
|
+
"node-telegram-bot-api": "^0.66.0",
|
|
32
33
|
"ws": "^8.18.0"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
package/preload/preload.js
CHANGED
|
@@ -62,4 +62,20 @@ contextBridge.exposeInMainWorld('clawmate', {
|
|
|
62
62
|
|
|
63
63
|
// AI 연결 상태 확인
|
|
64
64
|
isAIConnected: () => ipcRenderer.invoke('is-ai-connected'),
|
|
65
|
+
|
|
66
|
+
// 메트릭 보고 (렌더러 → main → OpenClaw)
|
|
67
|
+
reportMetrics: (summary) => ipcRenderer.send('report-metrics', summary),
|
|
68
|
+
|
|
69
|
+
// 활성 윈도우 제목 조회 (브라우저 감시)
|
|
70
|
+
getActiveWindowTitle: () => ipcRenderer.invoke('get-active-window-title'),
|
|
71
|
+
|
|
72
|
+
// 커서 위치 조회 (화면 좌표)
|
|
73
|
+
getCursorPosition: () => ipcRenderer.invoke('get-cursor-position'),
|
|
74
|
+
|
|
75
|
+
// === 스마트 파일 조작 ===
|
|
76
|
+
parseFileCommand: (text) => ipcRenderer.invoke('parse-file-command', text),
|
|
77
|
+
listFilteredFiles: (sourceDir, filter) => ipcRenderer.invoke('list-filtered-files', sourceDir, filter),
|
|
78
|
+
smartFileOp: (command) => ipcRenderer.invoke('smart-file-op', command),
|
|
79
|
+
undoSmartMove: (moveId) => ipcRenderer.invoke('undo-smart-move', moveId),
|
|
80
|
+
undoAllSmartMoves: () => ipcRenderer.invoke('undo-all-smart-moves'),
|
|
65
81
|
});
|
package/renderer/index.html
CHANGED
|
@@ -60,6 +60,8 @@
|
|
|
60
60
|
<script src="js/time-aware.js"></script>
|
|
61
61
|
<script src="js/mode-manager.js"></script>
|
|
62
62
|
<script src="js/memory.js"></script>
|
|
63
|
+
<script src="js/metrics.js"></script>
|
|
64
|
+
<script src="js/browser-watcher.js"></script>
|
|
63
65
|
<script src="js/ai-controller.js"></script>
|
|
64
66
|
<script src="js/app.js"></script>
|
|
65
67
|
</body>
|
|
@@ -140,9 +140,303 @@ const AIController = (() => {
|
|
|
140
140
|
// payload: { windowId, x, y }
|
|
141
141
|
PetEngine.jumpTo(payload.x, payload.y);
|
|
142
142
|
break;
|
|
143
|
+
|
|
144
|
+
// === 커스텀 이동 패턴 ===
|
|
145
|
+
|
|
146
|
+
case 'register_movement':
|
|
147
|
+
// OpenClaw이 JSON으로 이동 패턴 정의를 보내면 등록
|
|
148
|
+
// payload: { name, definition }
|
|
149
|
+
// definition: { type, params } — 각 타입별 파라미터
|
|
150
|
+
_registerAIMovement(payload.name, payload.definition);
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case 'custom_move':
|
|
154
|
+
// 등록된 커스텀 이동 패턴 실행
|
|
155
|
+
// payload: { name, params? }
|
|
156
|
+
if (!PetEngine.executeCustomMovement(payload.name, payload.params || {})) {
|
|
157
|
+
// 실행 실패 시 AI에 알림
|
|
158
|
+
if (window.clawmate.reportToAI) {
|
|
159
|
+
window.clawmate.reportToAI('custom_move_failed', {
|
|
160
|
+
name: payload.name,
|
|
161
|
+
available: PetEngine.getRegisteredMovements(),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case 'stop_custom_move':
|
|
168
|
+
// 현재 커스텀 이동 강제 중지
|
|
169
|
+
PetEngine.stopCustomMovement();
|
|
170
|
+
break;
|
|
171
|
+
|
|
172
|
+
case 'list_movements':
|
|
173
|
+
// 등록된 이동 패턴 목록 요청
|
|
174
|
+
if (window.clawmate.reportToAI) {
|
|
175
|
+
window.clawmate.reportToAI('movement_list', {
|
|
176
|
+
movements: PetEngine.getRegisteredMovements(),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
// === 캐릭터 커스터마이징 ===
|
|
182
|
+
case 'set_character':
|
|
183
|
+
// AI가 생성한 새 캐릭터 데이터 적용
|
|
184
|
+
Character.setCharacterData(payload);
|
|
185
|
+
if (payload.speech) {
|
|
186
|
+
Speech.show(payload.speech);
|
|
187
|
+
} else {
|
|
188
|
+
Speech.show('변신 완료!');
|
|
189
|
+
}
|
|
190
|
+
StateMachine.forceState('excited');
|
|
191
|
+
setTimeout(() => {
|
|
192
|
+
if (StateMachine.getState() === 'excited') StateMachine.forceState('idle');
|
|
193
|
+
}, 2000);
|
|
194
|
+
break;
|
|
195
|
+
|
|
196
|
+
case 'reset_character':
|
|
197
|
+
// 원래 캐릭터로 복원
|
|
198
|
+
Character.resetCharacter();
|
|
199
|
+
Speech.show('원래 모습으로 돌아왔어!');
|
|
200
|
+
StateMachine.forceState('excited');
|
|
201
|
+
break;
|
|
202
|
+
|
|
203
|
+
// === 스마트 파일 조작 애니메이션 ===
|
|
204
|
+
case 'smart_file_op':
|
|
205
|
+
handleSmartFileOp(payload);
|
|
206
|
+
break;
|
|
143
207
|
}
|
|
144
208
|
}
|
|
145
209
|
|
|
210
|
+
/**
|
|
211
|
+
* 스마트 파일 조작 애니메이션 처리
|
|
212
|
+
*
|
|
213
|
+
* 텔레그램이나 AI에서 트리거된 파일 이동 작업의
|
|
214
|
+
* 각 단계(phase)에 따라 펫 애니메이션을 순차 실행.
|
|
215
|
+
*
|
|
216
|
+
* phase:
|
|
217
|
+
* - start: 작업 시작, 총 파일 수 표시
|
|
218
|
+
* - pick_up: 파일 집어들기 (carrying 상태 + 말풍선)
|
|
219
|
+
* - drop: 파일 내려놓기 (걷기 상태 + 말풍선)
|
|
220
|
+
* - complete: 완료 (excited 상태 + 결과 말풍선)
|
|
221
|
+
* - error: 오류 (scared 상태 + 에러 말풍선)
|
|
222
|
+
*/
|
|
223
|
+
function handleSmartFileOp(payload) {
|
|
224
|
+
switch (payload.phase) {
|
|
225
|
+
case 'start':
|
|
226
|
+
StateMachine.forceState('excited');
|
|
227
|
+
Speech.show(`${payload.totalFiles}개 파일 정리 시작!`);
|
|
228
|
+
break;
|
|
229
|
+
|
|
230
|
+
case 'pick_up':
|
|
231
|
+
// 펫이 파일 위치로 이동 (화면 내 랜덤 위치)
|
|
232
|
+
_smartFileJumpToSource(payload.index);
|
|
233
|
+
// 집어들기 애니메이션
|
|
234
|
+
setTimeout(() => {
|
|
235
|
+
StateMachine.forceState('carrying');
|
|
236
|
+
Speech.show(`${payload.fileName} 집었다!`);
|
|
237
|
+
}, 400);
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case 'drop':
|
|
241
|
+
// 대상 폴더 위치로 이동
|
|
242
|
+
_smartFileJumpToTarget(payload.index);
|
|
243
|
+
// 내려놓기 애니메이션
|
|
244
|
+
setTimeout(() => {
|
|
245
|
+
StateMachine.forceState('walking');
|
|
246
|
+
Speech.show(`여기! (${payload.targetName})`);
|
|
247
|
+
}, 400);
|
|
248
|
+
break;
|
|
249
|
+
|
|
250
|
+
case 'complete':
|
|
251
|
+
StateMachine.forceState('excited');
|
|
252
|
+
if (payload.movedCount > 0) {
|
|
253
|
+
Speech.show(`${payload.movedCount}개 파일 옮겼어!`);
|
|
254
|
+
} else {
|
|
255
|
+
Speech.show('옮길 파일이 없었어!');
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
|
|
259
|
+
case 'error':
|
|
260
|
+
StateMachine.forceState('scared');
|
|
261
|
+
Speech.show('앗, 뭔가 잘못됐어...');
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 파일 집어들기 위치로 점프
|
|
268
|
+
* 파일 인덱스에 따라 화면 좌측 영역의 다른 위치로 이동
|
|
269
|
+
*/
|
|
270
|
+
function _smartFileJumpToSource(index) {
|
|
271
|
+
const screenW = window.innerWidth;
|
|
272
|
+
const screenH = window.innerHeight;
|
|
273
|
+
// 화면 왼쪽 1/3 영역에서 세로 위치를 파일 인덱스에 따라 분산
|
|
274
|
+
const targetX = screenW * 0.1 + (index % 3) * 50;
|
|
275
|
+
const targetY = screenH * 0.3 + ((index * 80) % (screenH * 0.5));
|
|
276
|
+
PetEngine.jumpTo(targetX, targetY);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 파일 내려놓기 위치로 점프
|
|
281
|
+
* 화면 오른쪽 영역으로 이동
|
|
282
|
+
*/
|
|
283
|
+
function _smartFileJumpToTarget(index) {
|
|
284
|
+
const screenW = window.innerWidth;
|
|
285
|
+
const screenH = window.innerHeight;
|
|
286
|
+
// 화면 오른쪽 1/3 영역
|
|
287
|
+
const targetX = screenW * 0.7 + (index % 3) * 50;
|
|
288
|
+
const targetY = screenH * 0.4 + ((index * 60) % (screenH * 0.4));
|
|
289
|
+
PetEngine.jumpTo(targetX, targetY);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* OpenClaw AI가 JSON으로 정의한 이동 패턴을 동적으로 등록
|
|
294
|
+
* 안전한 실행을 위해 Function 생성자 대신 사전정의된 행동 유형 조합 사용
|
|
295
|
+
*
|
|
296
|
+
* definition 형식:
|
|
297
|
+
* {
|
|
298
|
+
* type: 'waypoints' | 'formula' | 'sequence',
|
|
299
|
+
* waypoints?: [{x, y, pause?}], // waypoints 타입
|
|
300
|
+
* formula?: { xExpr, yExpr }, // formula 타입 (sin, cos 기반)
|
|
301
|
+
* sequence?: ['zigzag', 'shake', ...], // sequence 타입 (기존 패턴 순차 실행)
|
|
302
|
+
* duration?: number,
|
|
303
|
+
* speed?: number,
|
|
304
|
+
* }
|
|
305
|
+
*/
|
|
306
|
+
function _registerAIMovement(name, definition) {
|
|
307
|
+
if (!name || !definition || !definition.type) {
|
|
308
|
+
console.warn('[AIController] 이동 패턴 등록 실패: name, definition.type 필수');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let handler;
|
|
313
|
+
|
|
314
|
+
switch (definition.type) {
|
|
315
|
+
// 웨이포인트 타입: 지정된 좌표들을 순서대로 이동
|
|
316
|
+
case 'waypoints':
|
|
317
|
+
handler = {
|
|
318
|
+
init(params) {
|
|
319
|
+
return {
|
|
320
|
+
waypoints: definition.waypoints || [],
|
|
321
|
+
currentIdx: 0,
|
|
322
|
+
speed: definition.speed || 2,
|
|
323
|
+
pauseTime: 0,
|
|
324
|
+
pausing: false,
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
update(dt, state, ctx) {
|
|
328
|
+
if (state.currentIdx >= state.waypoints.length) return;
|
|
329
|
+
|
|
330
|
+
const wp = state.waypoints[state.currentIdx];
|
|
331
|
+
|
|
332
|
+
// 웨이포인트에서 멈춤 중
|
|
333
|
+
if (state.pausing) {
|
|
334
|
+
state.pauseTime -= dt;
|
|
335
|
+
if (state.pauseTime <= 0) {
|
|
336
|
+
state.pausing = false;
|
|
337
|
+
state.currentIdx++;
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const dx = wp.x - ctx.x;
|
|
343
|
+
const dy = wp.y - ctx.y;
|
|
344
|
+
const dist = Math.hypot(dx, dy);
|
|
345
|
+
|
|
346
|
+
if (dist < 5) {
|
|
347
|
+
// 웨이포인트 도달
|
|
348
|
+
if (wp.pause && wp.pause > 0) {
|
|
349
|
+
state.pausing = true;
|
|
350
|
+
state.pauseTime = wp.pause;
|
|
351
|
+
} else {
|
|
352
|
+
state.currentIdx++;
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const step = state.speed * (dt / 16);
|
|
358
|
+
const ratio = Math.min(1, step / dist);
|
|
359
|
+
ctx.setPos(ctx.x + dx * ratio, ctx.y + dy * ratio);
|
|
360
|
+
ctx.setFlip(dx < 0);
|
|
361
|
+
},
|
|
362
|
+
isComplete(state) {
|
|
363
|
+
return state.currentIdx >= (state.waypoints || []).length;
|
|
364
|
+
},
|
|
365
|
+
cleanup() {},
|
|
366
|
+
};
|
|
367
|
+
break;
|
|
368
|
+
|
|
369
|
+
// 수식 타입: sin/cos 기반 수학적 궤도
|
|
370
|
+
case 'formula':
|
|
371
|
+
handler = {
|
|
372
|
+
init(params) {
|
|
373
|
+
return {
|
|
374
|
+
duration: definition.duration || 3000,
|
|
375
|
+
elapsed: 0,
|
|
376
|
+
originX: params.x,
|
|
377
|
+
originY: params.y,
|
|
378
|
+
xAmp: definition.formula?.xAmp || 50,
|
|
379
|
+
yAmp: definition.formula?.yAmp || 30,
|
|
380
|
+
xFreq: definition.formula?.xFreq || 1,
|
|
381
|
+
yFreq: definition.formula?.yFreq || 1,
|
|
382
|
+
xPhase: definition.formula?.xPhase || 0,
|
|
383
|
+
yPhase: definition.formula?.yPhase || 0,
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
update(dt, state, ctx) {
|
|
387
|
+
state.elapsed += dt;
|
|
388
|
+
const t = (state.elapsed / state.duration) * Math.PI * 2;
|
|
389
|
+
const nx = state.originX + Math.sin(t * state.xFreq + state.xPhase) * state.xAmp;
|
|
390
|
+
const ny = state.originY + Math.sin(t * state.yFreq + state.yPhase) * state.yAmp;
|
|
391
|
+
ctx.setPos(nx, ny);
|
|
392
|
+
ctx.setFlip(Math.cos(t * state.xFreq + state.xPhase) < 0);
|
|
393
|
+
},
|
|
394
|
+
isComplete(state) {
|
|
395
|
+
return state.elapsed >= state.duration;
|
|
396
|
+
},
|
|
397
|
+
cleanup() {},
|
|
398
|
+
};
|
|
399
|
+
break;
|
|
400
|
+
|
|
401
|
+
// 시퀀스 타입: 기존 등록된 패턴들을 순차 실행
|
|
402
|
+
case 'sequence':
|
|
403
|
+
handler = {
|
|
404
|
+
init(params) {
|
|
405
|
+
return {
|
|
406
|
+
sequence: definition.sequence || [],
|
|
407
|
+
currentIdx: 0,
|
|
408
|
+
subStarted: false,
|
|
409
|
+
};
|
|
410
|
+
},
|
|
411
|
+
update(dt, state, ctx) {
|
|
412
|
+
if (state.currentIdx >= state.sequence.length) return;
|
|
413
|
+
|
|
414
|
+
if (!state.subStarted) {
|
|
415
|
+
const subName = state.sequence[state.currentIdx];
|
|
416
|
+
// 서브 패턴을 직접 실행하지 않고 상태만 추적
|
|
417
|
+
PetEngine.executeCustomMovement(subName, {
|
|
418
|
+
x: ctx.x, y: ctx.y,
|
|
419
|
+
screenW: ctx.screenW, screenH: ctx.screenH,
|
|
420
|
+
});
|
|
421
|
+
state.subStarted = true;
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
isComplete(state) {
|
|
425
|
+
return state.currentIdx >= (state.sequence || []).length;
|
|
426
|
+
},
|
|
427
|
+
cleanup() {},
|
|
428
|
+
};
|
|
429
|
+
break;
|
|
430
|
+
|
|
431
|
+
default:
|
|
432
|
+
console.warn(`[AIController] 알 수 없는 이동 패턴 타입: ${definition.type}`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
PetEngine.registerMovement(name, handler);
|
|
437
|
+
console.log(`[AIController] AI 이동 패턴 등록됨: ${name} (${definition.type})`);
|
|
438
|
+
}
|
|
439
|
+
|
|
146
440
|
/**
|
|
147
441
|
* AI 종합 의사결정 실행
|
|
148
442
|
* OpenClaw이 상황을 분석하고 내린 복합적 결정
|
package/renderer/js/app.js
CHANGED
|
@@ -71,6 +71,16 @@
|
|
|
71
71
|
// 시간 인식 초기화 (자율 모드에서만 주도적으로 동작)
|
|
72
72
|
TimeAware.init();
|
|
73
73
|
|
|
74
|
+
// 메트릭 수집기 초기화 (선택적 — 없어도 앱 정상 동작)
|
|
75
|
+
if (typeof Metrics !== 'undefined') {
|
|
76
|
+
Metrics.init();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 브라우저 감시 초기화 (참견쟁이 모드)
|
|
80
|
+
if (typeof BrowserWatcher !== 'undefined') {
|
|
81
|
+
BrowserWatcher.init();
|
|
82
|
+
}
|
|
83
|
+
|
|
74
84
|
// 엔진 시작
|
|
75
85
|
PetEngine.start();
|
|
76
86
|
|