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/ai-bridge.js
CHANGED
|
@@ -133,6 +133,12 @@ class AIBridge extends EventEmitter {
|
|
|
133
133
|
this.emit('drop_file', payload);
|
|
134
134
|
break;
|
|
135
135
|
|
|
136
|
+
case 'smart_file_op':
|
|
137
|
+
// 스마트 파일 조작 (텔레그램 또는 AI에서 트리거)
|
|
138
|
+
// payload: { phase: 'pick_up'|'drop'|'complete', fileName?, targetName?, ... }
|
|
139
|
+
this.emit('smart_file_op', payload);
|
|
140
|
+
break;
|
|
141
|
+
|
|
136
142
|
// === 외형 변화 ===
|
|
137
143
|
case 'evolve':
|
|
138
144
|
// 진화 트리거
|
|
@@ -188,6 +194,43 @@ class AIBridge extends EventEmitter {
|
|
|
188
194
|
this.emit('query_windows', payload);
|
|
189
195
|
break;
|
|
190
196
|
|
|
197
|
+
// === 커스텀 이동 패턴 ===
|
|
198
|
+
case 'register_movement':
|
|
199
|
+
// OpenClaw이 커스텀 이동 패턴 등록
|
|
200
|
+
// payload: { name: string, definition: { type: 'waypoints'|'formula'|'sequence', ... } }
|
|
201
|
+
this.emit('register_movement', payload);
|
|
202
|
+
break;
|
|
203
|
+
|
|
204
|
+
case 'custom_move':
|
|
205
|
+
// 등록된 커스텀 이동 패턴 실행
|
|
206
|
+
// payload: { name: string, params?: object }
|
|
207
|
+
this.emit('custom_move', payload);
|
|
208
|
+
break;
|
|
209
|
+
|
|
210
|
+
case 'stop_custom_move':
|
|
211
|
+
// 현재 커스텀 이동 강제 중지
|
|
212
|
+
// payload: {}
|
|
213
|
+
this.emit('stop_custom_move', payload);
|
|
214
|
+
break;
|
|
215
|
+
|
|
216
|
+
case 'list_movements':
|
|
217
|
+
// 등록된 이동 패턴 목록 요청 → 응답은 renderer에서 reportToAI로 전송
|
|
218
|
+
// payload: {}
|
|
219
|
+
this.emit('list_movements', payload);
|
|
220
|
+
break;
|
|
221
|
+
|
|
222
|
+
// === 캐릭터 커스터마이징 ===
|
|
223
|
+
case 'set_character':
|
|
224
|
+
// AI가 생성한 캐릭터 데이터 적용
|
|
225
|
+
// payload: { colorMap?: {...}, frames?: {...} }
|
|
226
|
+
this.emit('set_character', payload);
|
|
227
|
+
break;
|
|
228
|
+
|
|
229
|
+
case 'reset_character':
|
|
230
|
+
// 원래 캐릭터로 리셋
|
|
231
|
+
this.emit('reset_character', payload);
|
|
232
|
+
break;
|
|
233
|
+
|
|
191
234
|
// === 컨텍스트 질의 ===
|
|
192
235
|
case 'query_state':
|
|
193
236
|
// 현재 펫 상태 요청
|
|
@@ -285,6 +328,17 @@ class AIBridge extends EventEmitter {
|
|
|
285
328
|
});
|
|
286
329
|
}
|
|
287
330
|
|
|
331
|
+
/**
|
|
332
|
+
* 메트릭 데이터를 OpenClaw에 전송
|
|
333
|
+
* 렌더러에서 수집한 펫 동작 품질 메트릭을 AI에 전달
|
|
334
|
+
*/
|
|
335
|
+
reportMetrics(summary) {
|
|
336
|
+
this.send('metrics_report', {
|
|
337
|
+
metrics: summary,
|
|
338
|
+
timestamp: Date.now(),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
288
342
|
// === 상태 업데이트 ===
|
|
289
343
|
|
|
290
344
|
updatePetState(updates) {
|
package/main/ai-connector.js
CHANGED
|
@@ -95,6 +95,11 @@ class OpenClawConnector extends EventEmitter {
|
|
|
95
95
|
this.emit('window_positions', payload);
|
|
96
96
|
break;
|
|
97
97
|
|
|
98
|
+
case 'metrics_report':
|
|
99
|
+
// 메트릭 데이터 수신 → OpenClaw AI가 분석
|
|
100
|
+
this.emit('metrics_report', payload);
|
|
101
|
+
break;
|
|
102
|
+
|
|
98
103
|
case 'heartbeat':
|
|
99
104
|
break;
|
|
100
105
|
}
|
|
@@ -197,6 +202,74 @@ class OpenClawConnector extends EventEmitter {
|
|
|
197
202
|
return this._send('query_windows', {});
|
|
198
203
|
}
|
|
199
204
|
|
|
205
|
+
// === 커스텀 이동 패턴 API ===
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 커스텀 이동 패턴 등록
|
|
209
|
+
* ClawMate에 새로운 이동 패턴을 동적으로 추가
|
|
210
|
+
*
|
|
211
|
+
* @param {string} name - 패턴 이름 (예: 'figure8', 'spiral')
|
|
212
|
+
* @param {object} definition - 패턴 정의
|
|
213
|
+
* type: 'waypoints' | 'formula' | 'sequence'
|
|
214
|
+
* waypoints?: [{x, y, pause?}] — 웨이포인트 순차 이동
|
|
215
|
+
* formula?: { xAmp, yAmp, xFreq, yFreq, xPhase, yPhase } — 수학 궤도
|
|
216
|
+
* sequence?: ['zigzag', 'shake'] — 기존 패턴 순차 실행
|
|
217
|
+
* duration?: number — 지속 시간 (ms, formula 타입)
|
|
218
|
+
* speed?: number — 이동 속도
|
|
219
|
+
*
|
|
220
|
+
* 사용 예:
|
|
221
|
+
* connector.registerMovement('figure8', {
|
|
222
|
+
* type: 'formula',
|
|
223
|
+
* formula: { xAmp: 80, yAmp: 40, xFreq: 1, yFreq: 2 },
|
|
224
|
+
* duration: 4000,
|
|
225
|
+
* });
|
|
226
|
+
*/
|
|
227
|
+
registerMovement(name, definition) {
|
|
228
|
+
return this._send('register_movement', { name, definition });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 등록된 커스텀 이동 패턴 실행
|
|
233
|
+
*
|
|
234
|
+
* @param {string} name - 실행할 패턴 이름
|
|
235
|
+
* 사전 등록 패턴: 'zigzag', 'patrol', 'circle', 'shake', 'dance'
|
|
236
|
+
* 또는 registerMovement()로 등록한 커스텀 패턴
|
|
237
|
+
* @param {object} params - 실행 파라미터 (패턴별로 다름)
|
|
238
|
+
*
|
|
239
|
+
* 사용 예:
|
|
240
|
+
* connector.customMove('zigzag', { distance: 200, amplitude: 30 });
|
|
241
|
+
* connector.customMove('patrol', { pointAX: 100, pointBX: 500, laps: 5 });
|
|
242
|
+
* connector.customMove('shake', { intensity: 6, duration: 1000 });
|
|
243
|
+
*/
|
|
244
|
+
customMove(name, params = {}) {
|
|
245
|
+
return this._send('custom_move', { name, params });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** 현재 실행 중인 커스텀 이동 강제 중지 */
|
|
249
|
+
stopCustomMove() {
|
|
250
|
+
return this._send('stop_custom_move', {});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 등록된 이동 패턴 목록 요청 (응답은 user_event로 수신) */
|
|
254
|
+
listMovements() {
|
|
255
|
+
return this._send('list_movements', {});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 스마트 파일 조작 명령 전송 */
|
|
259
|
+
smartFileOp(payload) {
|
|
260
|
+
return this._send('smart_file_op', payload);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** 캐릭터 데이터 전송 (AI 생성 캐릭터 적용) */
|
|
264
|
+
setCharacter(data) {
|
|
265
|
+
return this._send('set_character', data);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** 원래 캐릭터로 리셋 */
|
|
269
|
+
resetCharacter() {
|
|
270
|
+
return this._send('reset_character', {});
|
|
271
|
+
}
|
|
272
|
+
|
|
200
273
|
/**
|
|
201
274
|
* 현재 펫 상태 요청 (Promise 반환)
|
|
202
275
|
* 서버에서 state_response가 오면 resolve, 타임아웃 시 캐시된 상태 반환
|
|
@@ -234,6 +307,11 @@ class OpenClawConnector extends EventEmitter {
|
|
|
234
307
|
this.on('user_event', callback);
|
|
235
308
|
}
|
|
236
309
|
|
|
310
|
+
/** 메트릭 리포트 리스너 등록 */
|
|
311
|
+
onMetrics(callback) {
|
|
312
|
+
this.on('metrics_report', callback);
|
|
313
|
+
}
|
|
314
|
+
|
|
237
315
|
disconnect() {
|
|
238
316
|
this._autoReconnect = false;
|
|
239
317
|
if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 파일 조작 명령 파서
|
|
3
|
+
*
|
|
4
|
+
* 한국어/영어 자연어를 파싱하여 파일 조작 의도를 추출한다.
|
|
5
|
+
* 텔레그램 메시지에서 파일 이동, 정리 등의 명령을 감지.
|
|
6
|
+
*
|
|
7
|
+
* 지원 패턴:
|
|
8
|
+
* - "바탕화면의 .md 파일을 tata 폴더에 넣어줘"
|
|
9
|
+
* - "스크린샷 폴더에 .png 정리해"
|
|
10
|
+
* - "바탕화면 정리해"
|
|
11
|
+
* - "move .txt files to docs folder"
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
// 알려진 소스 경로 별칭 (한국어 + 영어)
|
|
18
|
+
const SOURCE_ALIASES = {
|
|
19
|
+
'바탕화면': () => _getDesktopPath(),
|
|
20
|
+
'데스크탑': () => _getDesktopPath(),
|
|
21
|
+
'데스크톱': () => _getDesktopPath(),
|
|
22
|
+
'desktop': () => _getDesktopPath(),
|
|
23
|
+
'다운로드': () => path.join(os.homedir(), 'Downloads'),
|
|
24
|
+
'다운': () => path.join(os.homedir(), 'Downloads'),
|
|
25
|
+
'downloads': () => path.join(os.homedir(), 'Downloads'),
|
|
26
|
+
'문서': () => path.join(os.homedir(), 'Documents'),
|
|
27
|
+
'documents': () => path.join(os.homedir(), 'Documents'),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// 행동 명령 키워드 → 액션 매핑
|
|
31
|
+
const ACTION_KEYWORDS = {
|
|
32
|
+
'점프': 'jumping',
|
|
33
|
+
'점프해': 'jumping',
|
|
34
|
+
'뛰어': 'jumping',
|
|
35
|
+
'jump': 'jumping',
|
|
36
|
+
'잠자': 'sleeping',
|
|
37
|
+
'자': 'sleeping',
|
|
38
|
+
'잠잘래': 'sleeping',
|
|
39
|
+
'sleep': 'sleeping',
|
|
40
|
+
'춤춰': 'excited',
|
|
41
|
+
'춤': 'excited',
|
|
42
|
+
'dance': 'excited',
|
|
43
|
+
'걸어': 'walking',
|
|
44
|
+
'걸어다녀': 'walking',
|
|
45
|
+
'walk': 'walking',
|
|
46
|
+
'올라가': 'climbing_up',
|
|
47
|
+
'기어올라': 'climbing_up',
|
|
48
|
+
'climb': 'climbing_up',
|
|
49
|
+
'신나': 'excited',
|
|
50
|
+
'신나게': 'excited',
|
|
51
|
+
'놀아': 'playing',
|
|
52
|
+
'놀자': 'playing',
|
|
53
|
+
'play': 'playing',
|
|
54
|
+
'무서워': 'scared',
|
|
55
|
+
'깜짝': 'scared',
|
|
56
|
+
'레펠': 'rappelling',
|
|
57
|
+
'내려와': 'rappelling',
|
|
58
|
+
'rappel': 'rappelling',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// 파일 조작 감지 키워드
|
|
62
|
+
const FILE_OP_PATTERNS = [
|
|
63
|
+
// "~의 .ext 파일을 ~폴더에 넣어줘/옮겨줘/이동해"
|
|
64
|
+
/(?:(.+?)(?:의|에서|에 있는)\s+)?([.\w*]+)\s*파일(?:을|들을)?\s+(.+?)(?:폴더)?(?:에|으로)\s*(?:넣어|옮겨|이동|정리|보내)/,
|
|
65
|
+
// "~폴더에 .ext 정리해"
|
|
66
|
+
/(.+?)(?:폴더)?(?:에|으로)\s+([.\w*]+)\s*(?:파일\s*)?(?:정리|넣어|옮겨|이동)/,
|
|
67
|
+
// "바탕화면 정리해"
|
|
68
|
+
/(.+?)\s*(?:정리|청소|깔끔하게)\s*(?:해|해줘|하자|좀)/,
|
|
69
|
+
// 영어: "move .ext files to folder"
|
|
70
|
+
/move\s+([.\w*]+)\s+files?\s+(?:to|into)\s+(\S+)/i,
|
|
71
|
+
// 영어: "clean up desktop"
|
|
72
|
+
/clean\s*(?:up)?\s+(\S+)/i,
|
|
73
|
+
// 영어: "organize desktop"
|
|
74
|
+
/organize\s+(\S+)/i,
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 데스크톱 경로 가져오기 (file-ops와 동일 로직)
|
|
79
|
+
*/
|
|
80
|
+
function _getDesktopPath() {
|
|
81
|
+
try {
|
|
82
|
+
const { getDesktopPath } = require('./desktop-path');
|
|
83
|
+
return getDesktopPath();
|
|
84
|
+
} catch {
|
|
85
|
+
return path.join(os.homedir(), 'Desktop');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 소스 별칭을 실제 경로로 변환
|
|
91
|
+
* @param {string} alias - 소스 별칭 (예: "바탕화면")
|
|
92
|
+
* @returns {string|null} 실제 경로 또는 null
|
|
93
|
+
*/
|
|
94
|
+
function resolveSource(alias) {
|
|
95
|
+
if (!alias) return null;
|
|
96
|
+
const trimmed = alias.trim().toLowerCase();
|
|
97
|
+
for (const [key, resolver] of Object.entries(SOURCE_ALIASES)) {
|
|
98
|
+
if (trimmed === key || trimmed.includes(key)) {
|
|
99
|
+
return resolver();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 자동 분류 확장자 → 폴더명 매핑
|
|
107
|
+
* "바탕화면 정리해" 같은 범용 명령에서 사용
|
|
108
|
+
*/
|
|
109
|
+
const AUTO_CATEGORIES = {
|
|
110
|
+
'.png': '이미지',
|
|
111
|
+
'.jpg': '이미지',
|
|
112
|
+
'.jpeg': '이미지',
|
|
113
|
+
'.gif': '이미지',
|
|
114
|
+
'.bmp': '이미지',
|
|
115
|
+
'.webp': '이미지',
|
|
116
|
+
'.svg': '이미지',
|
|
117
|
+
'.pdf': '문서',
|
|
118
|
+
'.doc': '문서',
|
|
119
|
+
'.docx': '문서',
|
|
120
|
+
'.xlsx': '문서',
|
|
121
|
+
'.xls': '문서',
|
|
122
|
+
'.pptx': '문서',
|
|
123
|
+
'.ppt': '문서',
|
|
124
|
+
'.txt': '문서',
|
|
125
|
+
'.hwp': '문서',
|
|
126
|
+
'.md': '문서',
|
|
127
|
+
'.zip': '압축파일',
|
|
128
|
+
'.rar': '압축파일',
|
|
129
|
+
'.7z': '압축파일',
|
|
130
|
+
'.tar': '압축파일',
|
|
131
|
+
'.gz': '압축파일',
|
|
132
|
+
'.mp3': '음악',
|
|
133
|
+
'.wav': '음악',
|
|
134
|
+
'.flac': '음악',
|
|
135
|
+
'.aac': '음악',
|
|
136
|
+
'.mp4': '동영상',
|
|
137
|
+
'.avi': '동영상',
|
|
138
|
+
'.mkv': '동영상',
|
|
139
|
+
'.mov': '동영상',
|
|
140
|
+
'.wmv': '동영상',
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 메시지에서 행동 명령을 감지
|
|
145
|
+
* @param {string} text - 사용자 메시지
|
|
146
|
+
* @returns {{ type: 'action', action: string }|null}
|
|
147
|
+
*/
|
|
148
|
+
function parseActionCommand(text) {
|
|
149
|
+
if (!text) return null;
|
|
150
|
+
const trimmed = text.trim().toLowerCase();
|
|
151
|
+
|
|
152
|
+
for (const [keyword, action] of Object.entries(ACTION_KEYWORDS)) {
|
|
153
|
+
if (trimmed === keyword || trimmed.includes(keyword)) {
|
|
154
|
+
return { type: 'action', action };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 메시지에서 파일 조작 명령을 감지
|
|
162
|
+
* @param {string} text - 사용자 메시지
|
|
163
|
+
* @returns {{ type: 'smart_file_op', source: string, filter: string, target: string, autoCategory: boolean }|null}
|
|
164
|
+
*/
|
|
165
|
+
function parseFileCommand(text) {
|
|
166
|
+
if (!text) return null;
|
|
167
|
+
const trimmed = text.trim();
|
|
168
|
+
|
|
169
|
+
// 패턴 1: "바탕화면의 .md 파일을 tata 폴더에 넣어줘"
|
|
170
|
+
const pattern1 = /(?:(.+?)(?:의|에서|에 있는)\s+)?([.\w*]+)\s*파일(?:을|들을)?\s+(.+?)(?:\s*폴더)?(?:에|으로)\s*(?:넣어|옮겨|이동|정리|보내)/;
|
|
171
|
+
let match = trimmed.match(pattern1);
|
|
172
|
+
if (match) {
|
|
173
|
+
const sourceName = match[1] || '바탕화면';
|
|
174
|
+
const filter = match[2];
|
|
175
|
+
const target = match[3].trim();
|
|
176
|
+
const source = resolveSource(sourceName) || _getDesktopPath();
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
type: 'smart_file_op',
|
|
180
|
+
source,
|
|
181
|
+
filter: filter.startsWith('.') ? filter : `.${filter}`,
|
|
182
|
+
target,
|
|
183
|
+
autoCategory: false,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 패턴 2: "스크린샷 폴더에 .png 정리해"
|
|
188
|
+
const pattern2 = /(.+?)(?:\s*폴더)?(?:에|으로)\s+([.\w*]+)\s*(?:파일\s*)?(?:정리|넣어|옮겨|이동)/;
|
|
189
|
+
match = trimmed.match(pattern2);
|
|
190
|
+
if (match) {
|
|
191
|
+
const target = match[1].trim();
|
|
192
|
+
const filter = match[2];
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
type: 'smart_file_op',
|
|
196
|
+
source: _getDesktopPath(),
|
|
197
|
+
filter: filter.startsWith('.') ? filter : `.${filter}`,
|
|
198
|
+
target,
|
|
199
|
+
autoCategory: false,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 패턴 3: "바탕화면 정리해" (자동 분류)
|
|
204
|
+
const pattern3 = /(.+?)\s*(?:정리|청소|깔끔하게)\s*(?:해|해줘|하자|좀)?$/;
|
|
205
|
+
match = trimmed.match(pattern3);
|
|
206
|
+
if (match) {
|
|
207
|
+
const sourceName = match[1].trim();
|
|
208
|
+
const source = resolveSource(sourceName);
|
|
209
|
+
if (source) {
|
|
210
|
+
return {
|
|
211
|
+
type: 'smart_file_op',
|
|
212
|
+
source,
|
|
213
|
+
filter: '*',
|
|
214
|
+
target: 'auto',
|
|
215
|
+
autoCategory: true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 패턴 4 (영어): "move .txt files to docs"
|
|
221
|
+
const pattern4 = /move\s+([.\w*]+)\s+files?\s+(?:to|into)\s+(\S+)/i;
|
|
222
|
+
match = trimmed.match(pattern4);
|
|
223
|
+
if (match) {
|
|
224
|
+
const filter = match[1];
|
|
225
|
+
const target = match[2];
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
type: 'smart_file_op',
|
|
229
|
+
source: _getDesktopPath(),
|
|
230
|
+
filter: filter.startsWith('.') ? filter : `.${filter}`,
|
|
231
|
+
target,
|
|
232
|
+
autoCategory: false,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 패턴 5 (영어): "clean up desktop" / "organize desktop"
|
|
237
|
+
const pattern5 = /(?:clean\s*(?:up)?|organize)\s+(\S+)/i;
|
|
238
|
+
match = trimmed.match(pattern5);
|
|
239
|
+
if (match) {
|
|
240
|
+
const sourceName = match[1].trim();
|
|
241
|
+
const source = resolveSource(sourceName);
|
|
242
|
+
if (source) {
|
|
243
|
+
return {
|
|
244
|
+
type: 'smart_file_op',
|
|
245
|
+
source,
|
|
246
|
+
filter: '*',
|
|
247
|
+
target: 'auto',
|
|
248
|
+
autoCategory: true,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 캐릭터 변경 명령 감지
|
|
258
|
+
* @param {string} text - 사용자 메시지
|
|
259
|
+
* @returns {{ type: 'character_change', concept: string }|null}
|
|
260
|
+
*/
|
|
261
|
+
function parseCharacterCommand(text) {
|
|
262
|
+
if (!text) return null;
|
|
263
|
+
const trimmed = text.trim();
|
|
264
|
+
|
|
265
|
+
// 한국어 캐릭터 변경 패턴
|
|
266
|
+
const krPatterns = [
|
|
267
|
+
/(?:캐릭터|펫|모습|외형|외모)(?:를|을)?\s*(.+?)(?:로|으로)\s*(?:바꿔|변경|변신|만들어|바꿀래|바꾸고|바꿔줘|변경해|만들어줘)/,
|
|
268
|
+
/(.+?)(?:로|으로)\s*(?:캐릭터|펫|모습|외형)\s*(?:바꿔|변경|변신|변경해|바꿔줘)/,
|
|
269
|
+
/(.+?)\s*(?:캐릭터|펫)\s*(?:만들어|만들어줘|생성|생성해)/,
|
|
270
|
+
/(.+?)(?:로|으로)\s*변신(?:해|해줘|시켜|시켜줘)?/,
|
|
271
|
+
];
|
|
272
|
+
for (const pattern of krPatterns) {
|
|
273
|
+
const match = trimmed.match(pattern);
|
|
274
|
+
if (match) {
|
|
275
|
+
return { type: 'character_change', concept: match[1].trim() };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 영어 캐릭터 변경 패턴
|
|
280
|
+
const enPatterns = [
|
|
281
|
+
/(?:change|switch|transform)\s+(?:character|pet|look)\s+(?:to|into)\s+(.+)/i,
|
|
282
|
+
/(?:make|create|generate)\s+(?:a\s+)?(.+?)\s+(?:character|pet)/i,
|
|
283
|
+
/(?:become|turn into)\s+(?:a\s+)?(.+)/i,
|
|
284
|
+
];
|
|
285
|
+
for (const pattern of enPatterns) {
|
|
286
|
+
const match = trimmed.match(pattern);
|
|
287
|
+
if (match) {
|
|
288
|
+
return { type: 'character_change', concept: match[1].trim() };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 메시지 종합 파싱: 캐릭터 변경 > 파일 조작 > 행동 명령 > 일반 대화 순으로 판별
|
|
297
|
+
* @param {string} text - 사용자 메시지
|
|
298
|
+
* @returns {{ type: string, ... }}
|
|
299
|
+
*/
|
|
300
|
+
function parseMessage(text) {
|
|
301
|
+
// 0순위: 캐릭터 변경 명령
|
|
302
|
+
const charCmd = parseCharacterCommand(text);
|
|
303
|
+
if (charCmd) return charCmd;
|
|
304
|
+
|
|
305
|
+
// 1순위: 파일 조작 명령
|
|
306
|
+
const fileCmd = parseFileCommand(text);
|
|
307
|
+
if (fileCmd) return fileCmd;
|
|
308
|
+
|
|
309
|
+
// 2순위: 행동 명령
|
|
310
|
+
const actionCmd = parseActionCommand(text);
|
|
311
|
+
if (actionCmd) return actionCmd;
|
|
312
|
+
|
|
313
|
+
// 3순위: 일반 대화 (speak)
|
|
314
|
+
return { type: 'speak', text };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
module.exports = {
|
|
318
|
+
parseMessage,
|
|
319
|
+
parseFileCommand,
|
|
320
|
+
parseActionCommand,
|
|
321
|
+
parseCharacterCommand,
|
|
322
|
+
resolveSource,
|
|
323
|
+
AUTO_CATEGORIES,
|
|
324
|
+
};
|
package/main/index.js
CHANGED
|
@@ -3,10 +3,12 @@ const path = require('path');
|
|
|
3
3
|
const { setupTray } = require('./tray');
|
|
4
4
|
const { registerIpcHandlers } = require('./ipc-handlers');
|
|
5
5
|
const { AIBridge } = require('./ai-bridge');
|
|
6
|
+
const { TelegramBot } = require('./telegram');
|
|
6
7
|
|
|
7
8
|
let mainWindow = null;
|
|
8
9
|
let launcherWindow = null;
|
|
9
10
|
let aiBridge = null;
|
|
11
|
+
let telegramBot = null;
|
|
10
12
|
|
|
11
13
|
function createMainWindow() {
|
|
12
14
|
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
|
|
@@ -80,6 +82,12 @@ function startAIBridge(win) {
|
|
|
80
82
|
'accessorize', 'ai_decision',
|
|
81
83
|
// 공간 이동 명령 (OpenClaw이 집처럼 돌아다니기)
|
|
82
84
|
'jump_to', 'rappel', 'release_thread', 'move_to_center', 'walk_on_window',
|
|
85
|
+
// 커스텀 이동 패턴
|
|
86
|
+
'register_movement', 'custom_move', 'stop_custom_move', 'list_movements',
|
|
87
|
+
// 스마트 파일 조작 (텔레그램/AI에서 트리거한 파일 이동 애니메이션)
|
|
88
|
+
'smart_file_op',
|
|
89
|
+
// 캐릭터 커스터마이징 (텔레그램에서 AI 생성)
|
|
90
|
+
'set_character', 'reset_character',
|
|
83
91
|
];
|
|
84
92
|
|
|
85
93
|
commandTypes.forEach((type) => {
|
|
@@ -149,6 +157,9 @@ app.whenReady().then(() => {
|
|
|
149
157
|
const bridge = startAIBridge(win);
|
|
150
158
|
setupTray(win, bridge);
|
|
151
159
|
|
|
160
|
+
// 텔레그램 봇 초기화 (토큰 없으면 조용히 무시)
|
|
161
|
+
telegramBot = new TelegramBot(bridge);
|
|
162
|
+
|
|
152
163
|
// 최초 설치 시 자동 시작 등록
|
|
153
164
|
const { enableAutoStart, isAutoStartEnabled } = require('./autostart');
|
|
154
165
|
if (!isAutoStartEnabled()) {
|
|
@@ -165,6 +176,7 @@ app.on('window-all-closed', () => {
|
|
|
165
176
|
});
|
|
166
177
|
|
|
167
178
|
app.on('before-quit', () => {
|
|
179
|
+
if (telegramBot) telegramBot.stop();
|
|
168
180
|
if (aiBridge) aiBridge.stop();
|
|
169
181
|
});
|
|
170
182
|
|
package/main/ipc-handlers.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const { ipcMain, screen, desktopCapturer } = require('electron');
|
|
2
2
|
const { getDesktopFiles, moveFile, undoFileMove, undoAllMoves, getFileManifest } = require('./file-ops');
|
|
3
|
+
const { executeSmartFileOp, undoSmartMove, undoAllSmartMoves, listFilteredFiles } = require('./smart-file-ops');
|
|
4
|
+
const { parseMessage } = require('./file-command-parser');
|
|
3
5
|
const Store = require('./store');
|
|
4
6
|
|
|
5
7
|
const store = new Store('clawmate-config', {
|
|
@@ -119,6 +121,14 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
119
121
|
case 'user_idle':
|
|
120
122
|
bridge.reportIdleTime(data.idleSeconds);
|
|
121
123
|
break;
|
|
124
|
+
case 'browsing':
|
|
125
|
+
// 브라우징 컨텍스트 (제목 + 커서 위치 + 화면 캡처) → AI 코멘트 생성
|
|
126
|
+
bridge.send('user_event', { event: 'browsing', ...data });
|
|
127
|
+
break;
|
|
128
|
+
default:
|
|
129
|
+
// 알 수 없는 이벤트도 AI에 전달 (확장성)
|
|
130
|
+
bridge.send('user_event', { event, ...data });
|
|
131
|
+
break;
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
134
|
});
|
|
@@ -129,11 +139,103 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
129
139
|
return bridge ? bridge.isConnected() : false;
|
|
130
140
|
});
|
|
131
141
|
|
|
142
|
+
// 메트릭 보고 (렌더러 → main → OpenClaw)
|
|
143
|
+
ipcMain.on('report-metrics', (_, summary) => {
|
|
144
|
+
const bridge = getAIBridge();
|
|
145
|
+
if (bridge && bridge.isConnected()) {
|
|
146
|
+
bridge.reportMetrics(summary);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
132
150
|
// 열린 윈도우 위치/크기 조회
|
|
133
151
|
ipcMain.handle('get-window-positions', async () => {
|
|
134
152
|
const { getWindowPositions } = require('./platform');
|
|
135
153
|
return await getWindowPositions();
|
|
136
154
|
});
|
|
155
|
+
|
|
156
|
+
// 활성 윈도우 제목 조회 (브라우저 감시용)
|
|
157
|
+
ipcMain.handle('get-active-window-title', async () => {
|
|
158
|
+
const { getActiveWindowTitle } = require('./platform');
|
|
159
|
+
return await getActiveWindowTitle();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// 커서 위치 조회 (화면 좌표)
|
|
163
|
+
ipcMain.handle('get-cursor-position', () => {
|
|
164
|
+
const point = screen.getCursorScreenPoint();
|
|
165
|
+
return { x: point.x, y: point.y };
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// === 스마트 파일 조작 IPC ===
|
|
169
|
+
|
|
170
|
+
// 파일 명령 파싱 (렌더러에서도 사용 가능)
|
|
171
|
+
ipcMain.handle('parse-file-command', (_, text) => {
|
|
172
|
+
return parseMessage(text);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// 필터된 파일 목록 조회
|
|
176
|
+
ipcMain.handle('list-filtered-files', async (_, sourceDir, filter) => {
|
|
177
|
+
return listFilteredFiles(sourceDir, filter);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// 스마트 파일 조작 실행
|
|
181
|
+
// 렌더러에서 직접 실행할 때 사용 (텔레그램 경유가 아닌 경우)
|
|
182
|
+
ipcMain.handle('smart-file-op', async (_, command) => {
|
|
183
|
+
const win = getMainWindow();
|
|
184
|
+
const callbacks = {
|
|
185
|
+
onStart: (totalFiles) => {
|
|
186
|
+
if (win && !win.isDestroyed()) {
|
|
187
|
+
win.webContents.send('ai-command', {
|
|
188
|
+
type: 'smart_file_op',
|
|
189
|
+
payload: { phase: 'start', totalFiles },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
onPickUp: (fileName, index) => {
|
|
194
|
+
if (win && !win.isDestroyed()) {
|
|
195
|
+
win.webContents.send('ai-command', {
|
|
196
|
+
type: 'smart_file_op',
|
|
197
|
+
payload: { phase: 'pick_up', fileName, index },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
onDrop: (fileName, targetName, index) => {
|
|
202
|
+
if (win && !win.isDestroyed()) {
|
|
203
|
+
win.webContents.send('ai-command', {
|
|
204
|
+
type: 'smart_file_op',
|
|
205
|
+
payload: { phase: 'drop', fileName, targetName, index },
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
onComplete: (result) => {
|
|
210
|
+
if (win && !win.isDestroyed()) {
|
|
211
|
+
win.webContents.send('ai-command', {
|
|
212
|
+
type: 'smart_file_op',
|
|
213
|
+
payload: { phase: 'complete', ...result },
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
onError: (error) => {
|
|
218
|
+
if (win && !win.isDestroyed()) {
|
|
219
|
+
win.webContents.send('ai-command', {
|
|
220
|
+
type: 'smart_file_op',
|
|
221
|
+
payload: { phase: 'error', error },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return await executeSmartFileOp(command, callbacks);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// 스마트 이동 되돌리기 (단일)
|
|
231
|
+
ipcMain.handle('undo-smart-move', async (_, moveId) => {
|
|
232
|
+
return undoSmartMove(moveId);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// 스마트 이동 전체 되돌리기
|
|
236
|
+
ipcMain.handle('undo-all-smart-moves', async () => {
|
|
237
|
+
return undoAllSmartMoves();
|
|
238
|
+
});
|
|
137
239
|
}
|
|
138
240
|
|
|
139
241
|
module.exports = { registerIpcHandlers };
|