clawmate 1.2.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 +945 -17
- package/main/ai-bridge.js +76 -16
- package/main/ai-connector.js +94 -11
- package/main/autostart.js +3 -3
- package/main/file-command-parser.js +360 -0
- package/main/index.js +19 -5
- package/main/ipc-handlers.js +107 -2
- package/main/platform.js +48 -1
- package/main/smart-file-ops.js +373 -0
- package/main/telegram.js +593 -0
- package/main/tray.js +307 -39
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -4
- package/preload/preload.js +19 -3
- package/renderer/first-run.html +2 -2
- package/renderer/index.html +2 -0
- package/renderer/js/ai-controller.js +312 -7
- package/renderer/js/app.js +19 -6
- package/renderer/js/browser-watcher.js +172 -0
- package/renderer/js/character.js +119 -22
- package/renderer/js/interactions.js +45 -2
- package/renderer/js/memory.js +108 -1
- package/renderer/js/metrics.js +607 -0
- package/renderer/js/mode-manager.js +53 -9
- package/renderer/js/pet-engine.js +372 -30
- package/renderer/js/state-machine.js +7 -0
- package/renderer/launcher.html +3 -3
- package/shared/messages.js +110 -0
- package/shared/personalities.js +37 -2
- package/skills/launch-pet/index.js +1 -1
- package/skills/launch-pet/skill.json +1 -1
|
@@ -0,0 +1,360 @@
|
|
|
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
|
+
/**
|
|
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
|
+
|
|
331
|
+
function parseMessage(text) {
|
|
332
|
+
// 0순위: 설정/모드 변경 명령
|
|
333
|
+
const settingCmd = parseSettingCommand(text);
|
|
334
|
+
if (settingCmd) return settingCmd;
|
|
335
|
+
|
|
336
|
+
// 1순위: 캐릭터 변경 명령 (AI 생성)
|
|
337
|
+
const charCmd = parseCharacterCommand(text);
|
|
338
|
+
if (charCmd) return charCmd;
|
|
339
|
+
|
|
340
|
+
// 2순위: 파일 조작 명령
|
|
341
|
+
const fileCmd = parseFileCommand(text);
|
|
342
|
+
if (fileCmd) return fileCmd;
|
|
343
|
+
|
|
344
|
+
// 3순위: 행동 명령
|
|
345
|
+
const actionCmd = parseActionCommand(text);
|
|
346
|
+
if (actionCmd) return actionCmd;
|
|
347
|
+
|
|
348
|
+
// 4순위: 일반 대화 (speak)
|
|
349
|
+
return { type: 'speak', text };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = {
|
|
353
|
+
parseMessage,
|
|
354
|
+
parseFileCommand,
|
|
355
|
+
parseActionCommand,
|
|
356
|
+
parseCharacterCommand,
|
|
357
|
+
parseSettingCommand,
|
|
358
|
+
resolveSource,
|
|
359
|
+
AUTO_CATEGORIES,
|
|
360
|
+
};
|
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;
|
|
@@ -67,19 +69,27 @@ function createLauncherWindow() {
|
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
/**
|
|
70
|
-
* AI Bridge 시작 —
|
|
72
|
+
* AI Bridge 시작 — AI 에이전트가 접속하면 펫을 조종
|
|
71
73
|
*/
|
|
72
74
|
function startAIBridge(win) {
|
|
73
75
|
aiBridge = new AIBridge();
|
|
74
76
|
aiBridge.start();
|
|
75
77
|
|
|
76
|
-
//
|
|
78
|
+
// AI → ClawMate 명령을 렌더러에 전달
|
|
77
79
|
const commandTypes = [
|
|
78
80
|
'action', 'move', 'emote', 'speak', 'think',
|
|
79
81
|
'carry_file', 'drop_file', 'set_mode', 'evolve',
|
|
80
82
|
'accessorize', 'ai_decision',
|
|
81
|
-
// 공간 이동 명령 (
|
|
83
|
+
// 공간 이동 명령 (펫이 집처럼 돌아다니기)
|
|
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',
|
|
91
|
+
// 인격체 전환 (Incarnation 모드)
|
|
92
|
+
'set_persona',
|
|
83
93
|
];
|
|
84
94
|
|
|
85
95
|
commandTypes.forEach((type) => {
|
|
@@ -90,7 +100,7 @@ function startAIBridge(win) {
|
|
|
90
100
|
});
|
|
91
101
|
});
|
|
92
102
|
|
|
93
|
-
//
|
|
103
|
+
// AI 윈도우 위치 정보 요청 처리
|
|
94
104
|
aiBridge.on('query_windows', async () => {
|
|
95
105
|
try {
|
|
96
106
|
const { getWindowPositions } = require('./platform');
|
|
@@ -102,7 +112,7 @@ function startAIBridge(win) {
|
|
|
102
112
|
}
|
|
103
113
|
});
|
|
104
114
|
|
|
105
|
-
//
|
|
115
|
+
// AI 화면 캡처 요청 처리 (main process에서 직접 캡처)
|
|
106
116
|
aiBridge.on('query_screen', async () => {
|
|
107
117
|
try {
|
|
108
118
|
const primaryDisplay = screen.getPrimaryDisplay();
|
|
@@ -149,6 +159,9 @@ app.whenReady().then(() => {
|
|
|
149
159
|
const bridge = startAIBridge(win);
|
|
150
160
|
setupTray(win, bridge);
|
|
151
161
|
|
|
162
|
+
// 텔레그램 봇 초기화 (토큰 없으면 조용히 무시)
|
|
163
|
+
telegramBot = new TelegramBot(bridge);
|
|
164
|
+
|
|
152
165
|
// 최초 설치 시 자동 시작 등록
|
|
153
166
|
const { enableAutoStart, isAutoStartEnabled } = require('./autostart');
|
|
154
167
|
if (!isAutoStartEnabled()) {
|
|
@@ -165,6 +178,7 @@ app.on('window-all-closed', () => {
|
|
|
165
178
|
});
|
|
166
179
|
|
|
167
180
|
app.on('before-quit', () => {
|
|
181
|
+
if (telegramBot) telegramBot.stop();
|
|
168
182
|
if (aiBridge) aiBridge.stop();
|
|
169
183
|
});
|
|
170
184
|
|
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', {
|
|
@@ -91,9 +93,9 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
91
93
|
}
|
|
92
94
|
});
|
|
93
95
|
|
|
94
|
-
// ===
|
|
96
|
+
// === AI 통신 ===
|
|
95
97
|
|
|
96
|
-
// 사용자 이벤트를 AI Bridge로 전달 (렌더러 → main →
|
|
98
|
+
// 사용자 이벤트를 AI Bridge로 전달 (렌더러 → main → AI)
|
|
97
99
|
ipcMain.on('report-to-ai', (_, event, data) => {
|
|
98
100
|
const bridge = getAIBridge();
|
|
99
101
|
if (bridge && bridge.isConnected()) {
|
|
@@ -107,6 +109,9 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
107
109
|
case 'cursor_near':
|
|
108
110
|
bridge.reportCursorNear(data.distance, data.cursorPos);
|
|
109
111
|
break;
|
|
112
|
+
case 'double_click':
|
|
113
|
+
bridge.send('user_event', { event: 'double_click', ...data });
|
|
114
|
+
break;
|
|
110
115
|
case 'desktop_changed':
|
|
111
116
|
bridge.reportDesktopChange(data.files);
|
|
112
117
|
break;
|
|
@@ -119,6 +124,14 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
119
124
|
case 'user_idle':
|
|
120
125
|
bridge.reportIdleTime(data.idleSeconds);
|
|
121
126
|
break;
|
|
127
|
+
case 'browsing':
|
|
128
|
+
// 브라우징 컨텍스트 (제목 + 커서 위치 + 화면 캡처) → AI 코멘트 생성
|
|
129
|
+
bridge.send('user_event', { event: 'browsing', ...data });
|
|
130
|
+
break;
|
|
131
|
+
default:
|
|
132
|
+
// 알 수 없는 이벤트도 AI에 전달 (확장성)
|
|
133
|
+
bridge.send('user_event', { event, ...data });
|
|
134
|
+
break;
|
|
122
135
|
}
|
|
123
136
|
}
|
|
124
137
|
});
|
|
@@ -129,11 +142,103 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
129
142
|
return bridge ? bridge.isConnected() : false;
|
|
130
143
|
});
|
|
131
144
|
|
|
145
|
+
// 메트릭 보고 (렌더러 → main → AI)
|
|
146
|
+
ipcMain.on('report-metrics', (_, summary) => {
|
|
147
|
+
const bridge = getAIBridge();
|
|
148
|
+
if (bridge && bridge.isConnected()) {
|
|
149
|
+
bridge.reportMetrics(summary);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
132
153
|
// 열린 윈도우 위치/크기 조회
|
|
133
154
|
ipcMain.handle('get-window-positions', async () => {
|
|
134
155
|
const { getWindowPositions } = require('./platform');
|
|
135
156
|
return await getWindowPositions();
|
|
136
157
|
});
|
|
158
|
+
|
|
159
|
+
// 활성 윈도우 제목 조회 (브라우저 감시용)
|
|
160
|
+
ipcMain.handle('get-active-window-title', async () => {
|
|
161
|
+
const { getActiveWindowTitle } = require('./platform');
|
|
162
|
+
return await getActiveWindowTitle();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// 커서 위치 조회 (화면 좌표)
|
|
166
|
+
ipcMain.handle('get-cursor-position', () => {
|
|
167
|
+
const point = screen.getCursorScreenPoint();
|
|
168
|
+
return { x: point.x, y: point.y };
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// === 스마트 파일 조작 IPC ===
|
|
172
|
+
|
|
173
|
+
// 파일 명령 파싱 (렌더러에서도 사용 가능)
|
|
174
|
+
ipcMain.handle('parse-file-command', (_, text) => {
|
|
175
|
+
return parseMessage(text);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// 필터된 파일 목록 조회
|
|
179
|
+
ipcMain.handle('list-filtered-files', async (_, sourceDir, filter) => {
|
|
180
|
+
return listFilteredFiles(sourceDir, filter);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// 스마트 파일 조작 실행
|
|
184
|
+
// 렌더러에서 직접 실행할 때 사용 (텔레그램 경유가 아닌 경우)
|
|
185
|
+
ipcMain.handle('smart-file-op', async (_, command) => {
|
|
186
|
+
const win = getMainWindow();
|
|
187
|
+
const callbacks = {
|
|
188
|
+
onStart: (totalFiles) => {
|
|
189
|
+
if (win && !win.isDestroyed()) {
|
|
190
|
+
win.webContents.send('ai-command', {
|
|
191
|
+
type: 'smart_file_op',
|
|
192
|
+
payload: { phase: 'start', totalFiles },
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
onPickUp: (fileName, index) => {
|
|
197
|
+
if (win && !win.isDestroyed()) {
|
|
198
|
+
win.webContents.send('ai-command', {
|
|
199
|
+
type: 'smart_file_op',
|
|
200
|
+
payload: { phase: 'pick_up', fileName, index },
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
onDrop: (fileName, targetName, index) => {
|
|
205
|
+
if (win && !win.isDestroyed()) {
|
|
206
|
+
win.webContents.send('ai-command', {
|
|
207
|
+
type: 'smart_file_op',
|
|
208
|
+
payload: { phase: 'drop', fileName, targetName, index },
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
onComplete: (result) => {
|
|
213
|
+
if (win && !win.isDestroyed()) {
|
|
214
|
+
win.webContents.send('ai-command', {
|
|
215
|
+
type: 'smart_file_op',
|
|
216
|
+
payload: { phase: 'complete', ...result },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
onError: (error) => {
|
|
221
|
+
if (win && !win.isDestroyed()) {
|
|
222
|
+
win.webContents.send('ai-command', {
|
|
223
|
+
type: 'smart_file_op',
|
|
224
|
+
payload: { phase: 'error', error },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return await executeSmartFileOp(command, callbacks);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// 스마트 이동 되돌리기 (단일)
|
|
234
|
+
ipcMain.handle('undo-smart-move', async (_, moveId) => {
|
|
235
|
+
return undoSmartMove(moveId);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// 스마트 이동 전체 되돌리기
|
|
239
|
+
ipcMain.handle('undo-all-smart-moves', async () => {
|
|
240
|
+
return undoAllSmartMoves();
|
|
241
|
+
});
|
|
137
242
|
}
|
|
138
243
|
|
|
139
244
|
module.exports = { registerIpcHandlers };
|
package/main/platform.js
CHANGED
|
@@ -213,4 +213,51 @@ async function getWindowPositionsLinux() {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
|
|
216
|
+
/**
|
|
217
|
+
* 현재 포커스된 (최상위) 윈도우의 제목 반환
|
|
218
|
+
* 브라우저 탭 제목을 감지하여 펫이 참견할 수 있게 함
|
|
219
|
+
*/
|
|
220
|
+
async function getActiveWindowTitle() {
|
|
221
|
+
try {
|
|
222
|
+
if (platform === 'win32') {
|
|
223
|
+
const psScript = `
|
|
224
|
+
Add-Type @"
|
|
225
|
+
using System;
|
|
226
|
+
using System.Runtime.InteropServices;
|
|
227
|
+
using System.Text;
|
|
228
|
+
public class FGWin {
|
|
229
|
+
[DllImport("user32.dll")] static extern IntPtr GetForegroundWindow();
|
|
230
|
+
[DllImport("user32.dll")] static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
|
|
231
|
+
public static string Get() {
|
|
232
|
+
IntPtr h = GetForegroundWindow();
|
|
233
|
+
StringBuilder sb = new StringBuilder(512);
|
|
234
|
+
GetWindowText(h, sb, 512);
|
|
235
|
+
return sb.ToString();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
"@
|
|
239
|
+
[FGWin]::Get()
|
|
240
|
+
`.trim();
|
|
241
|
+
const { stdout } = await execAsync(
|
|
242
|
+
`powershell -NoProfile -Command -`,
|
|
243
|
+
{ input: psScript, timeout: 3000, encoding: 'utf-8' }
|
|
244
|
+
);
|
|
245
|
+
return (stdout || '').trim();
|
|
246
|
+
} else if (platform === 'darwin') {
|
|
247
|
+
const { stdout } = await execAsync(
|
|
248
|
+
`osascript -e 'tell application "System Events" to get name of first window of (first process whose frontmost is true)'`,
|
|
249
|
+
{ timeout: 3000, encoding: 'utf-8' }
|
|
250
|
+
);
|
|
251
|
+
return (stdout || '').trim();
|
|
252
|
+
} else {
|
|
253
|
+
const { stdout } = await execAsync('xdotool getactivewindow getwindowname', {
|
|
254
|
+
timeout: 3000, encoding: 'utf-8',
|
|
255
|
+
});
|
|
256
|
+
return (stdout || '').trim();
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
return '';
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = { getDesktopPath, getTrayIconExt, isWindows, isMac, isLinux, platform, getWindowPositions, getActiveWindowTitle };
|