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/main/ipc-handlers.js
CHANGED
|
@@ -93,9 +93,9 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
93
93
|
}
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
// ===
|
|
96
|
+
// === AI 통신 ===
|
|
97
97
|
|
|
98
|
-
// 사용자 이벤트를 AI Bridge로 전달 (렌더러 → main →
|
|
98
|
+
// 사용자 이벤트를 AI Bridge로 전달 (렌더러 → main → AI)
|
|
99
99
|
ipcMain.on('report-to-ai', (_, event, data) => {
|
|
100
100
|
const bridge = getAIBridge();
|
|
101
101
|
if (bridge && bridge.isConnected()) {
|
|
@@ -109,6 +109,9 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
109
109
|
case 'cursor_near':
|
|
110
110
|
bridge.reportCursorNear(data.distance, data.cursorPos);
|
|
111
111
|
break;
|
|
112
|
+
case 'double_click':
|
|
113
|
+
bridge.send('user_event', { event: 'double_click', ...data });
|
|
114
|
+
break;
|
|
112
115
|
case 'desktop_changed':
|
|
113
116
|
bridge.reportDesktopChange(data.files);
|
|
114
117
|
break;
|
|
@@ -139,7 +142,7 @@ function registerIpcHandlers(getMainWindow, getAIBridge) {
|
|
|
139
142
|
return bridge ? bridge.isConnected() : false;
|
|
140
143
|
});
|
|
141
144
|
|
|
142
|
-
// 메트릭 보고 (렌더러 → main →
|
|
145
|
+
// 메트릭 보고 (렌더러 → main → AI)
|
|
143
146
|
ipcMain.on('report-metrics', (_, summary) => {
|
|
144
147
|
const bridge = getAIBridge();
|
|
145
148
|
if (bridge && bridge.isConnected()) {
|
package/main/telegram.js
CHANGED
|
@@ -196,13 +196,46 @@ class TelegramBot extends EventEmitter {
|
|
|
196
196
|
// 캐릭터 변경 명령 → AI 생성 요청
|
|
197
197
|
await this._handleCharacterChange(chatId, command.concept);
|
|
198
198
|
break;
|
|
199
|
+
|
|
200
|
+
case 'mode_change':
|
|
201
|
+
// 모드 변경 명령
|
|
202
|
+
this._sendToBridge('set_mode', { mode: command.mode });
|
|
203
|
+
const modeNames = { pet: 'Pet (Clawby)', incarnation: 'Incarnation (Claw)', both: '둘 다' };
|
|
204
|
+
await this.bot.sendMessage(chatId, `모드 변경: ${modeNames[command.mode] || command.mode}`);
|
|
205
|
+
break;
|
|
206
|
+
|
|
207
|
+
case 'preset_character': {
|
|
208
|
+
// 캐릭터 프리셋 선택
|
|
209
|
+
const presets = {
|
|
210
|
+
default: { name: '기본 Claw', colorMap: { primary: '#ff4f40', secondary: '#ff775f', dark: '#8B4513', eye: '#ffffff', pupil: '#111111', claw: '#ff4f40' } },
|
|
211
|
+
blue: { name: '파란 Claw', colorMap: { primary: '#4488ff', secondary: '#6699ff', dark: '#223388', eye: '#ffffff', pupil: '#111111', claw: '#4488ff' } },
|
|
212
|
+
green: { name: '초록 Claw', colorMap: { primary: '#44cc44', secondary: '#66dd66', dark: '#226622', eye: '#ffffff', pupil: '#111111', claw: '#44cc44' } },
|
|
213
|
+
purple: { name: '보라 Claw', colorMap: { primary: '#8844cc', secondary: '#aa66dd', dark: '#442266', eye: '#ffffff', pupil: '#111111', claw: '#8844cc' } },
|
|
214
|
+
gold: { name: '골드 Claw', colorMap: { primary: '#ffcc00', secondary: '#ffdd44', dark: '#886600', eye: '#ffffff', pupil: '#111111', claw: '#ffcc00' } },
|
|
215
|
+
pink: { name: '핑크 Claw', colorMap: { primary: '#ff69b4', secondary: '#ff8cc4', dark: '#8B3060', eye: '#ffffff', pupil: '#111111', claw: '#ff69b4' } },
|
|
216
|
+
cat: { name: '고양이', colorMap: { primary: '#ff9944', secondary: '#ffbb66', dark: '#663300', eye: '#88ff88', pupil: '#111111', claw: '#ff9944' } },
|
|
217
|
+
robot: { name: '로봇', colorMap: { primary: '#888888', secondary: '#aaaaaa', dark: '#444444', eye: '#66aaff', pupil: '#0044aa', claw: '#66aaff' } },
|
|
218
|
+
ghost: { name: '유령', colorMap: { primary: '#ccccff', secondary: '#eeeeff', dark: '#6666aa', eye: '#ff6666', pupil: '#cc0000', claw: '#ccccff' } },
|
|
219
|
+
dragon: { name: '드래곤', colorMap: { primary: '#cc2222', secondary: '#ff4444', dark: '#661111', eye: '#ffaa00', pupil: '#111111', claw: '#ffaa00' } },
|
|
220
|
+
};
|
|
221
|
+
const preset = presets[command.preset];
|
|
222
|
+
if (preset) {
|
|
223
|
+
if (command.preset === 'default') {
|
|
224
|
+
this._sendToBridge('reset_character', {});
|
|
225
|
+
} else {
|
|
226
|
+
this._sendToBridge('set_character', { colorMap: preset.colorMap, speech: `${preset.name}(으)로 변신!` });
|
|
227
|
+
}
|
|
228
|
+
await this.bot.sendMessage(chatId, `캐릭터 변경: ${preset.name}`);
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
199
232
|
}
|
|
200
233
|
}
|
|
201
234
|
|
|
202
235
|
/**
|
|
203
236
|
* 캐릭터 변경 요청 처리
|
|
204
237
|
*
|
|
205
|
-
* 컨셉 텍스트를 AI
|
|
238
|
+
* 컨셉 텍스트를 AI에 전달하여
|
|
206
239
|
* 색상 + 프레임 데이터를 생성하고 펫에 적용.
|
|
207
240
|
*
|
|
208
241
|
* AI가 없으면 컨셉에서 색상만 추출하여 기본 변환.
|
|
@@ -210,14 +243,14 @@ class TelegramBot extends EventEmitter {
|
|
|
210
243
|
async _handleCharacterChange(chatId, concept) {
|
|
211
244
|
await this.bot.sendMessage(chatId, `"${concept}" 캐릭터 생성 중...`);
|
|
212
245
|
|
|
213
|
-
// AI Bridge를 통해
|
|
246
|
+
// AI Bridge를 통해 AI에 캐릭터 생성 요청
|
|
214
247
|
this._sendToBridge('ai_decision', {
|
|
215
248
|
speech: `${concept}(으)로 변신 준비 중...`,
|
|
216
249
|
emotion: 'curious',
|
|
217
250
|
action: 'excited',
|
|
218
251
|
});
|
|
219
252
|
|
|
220
|
-
// user_event로 캐릭터 변경 요청 전달 (
|
|
253
|
+
// user_event로 캐릭터 변경 요청 전달 (AI가 생성)
|
|
221
254
|
if (this.bridge) {
|
|
222
255
|
this.bridge.send('user_event', {
|
|
223
256
|
event: 'character_request',
|
package/main/tray.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { Tray, Menu, nativeImage, app, shell } = require('electron');
|
|
1
|
+
const { Tray, Menu, nativeImage, app, shell, dialog, clipboard } = require('electron');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const Store = require('./store');
|
|
@@ -10,16 +10,6 @@ let aiBridge = null;
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
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
13
|
*/
|
|
24
14
|
const CLAW_ICON = [
|
|
25
15
|
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
|
|
@@ -41,44 +31,88 @@ const CLAW_ICON = [
|
|
|
41
31
|
];
|
|
42
32
|
|
|
43
33
|
const COLOR_MAP = {
|
|
44
|
-
0: [0, 0, 0, 0],
|
|
45
|
-
1: [255, 79, 64, 255],
|
|
46
|
-
2: [255, 119, 95, 255],
|
|
47
|
-
3: [58, 10, 13, 255],
|
|
48
|
-
4: [255, 255, 255, 255],
|
|
49
|
-
5: [0, 0, 0, 255],
|
|
50
|
-
6: [255, 79, 64, 255],
|
|
34
|
+
0: [0, 0, 0, 0],
|
|
35
|
+
1: [255, 79, 64, 255],
|
|
36
|
+
2: [255, 119, 95, 255],
|
|
37
|
+
3: [58, 10, 13, 255],
|
|
38
|
+
4: [255, 255, 255, 255],
|
|
39
|
+
5: [0, 0, 0, 255],
|
|
40
|
+
6: [255, 79, 64, 255],
|
|
51
41
|
};
|
|
52
42
|
|
|
53
43
|
/**
|
|
54
|
-
*
|
|
44
|
+
* 캐릭터 프리셋 목록
|
|
45
|
+
* 트레이에서 선택하면 set_character 명령으로 렌더러에 전달
|
|
55
46
|
*/
|
|
47
|
+
const CHARACTER_PRESETS = {
|
|
48
|
+
default: {
|
|
49
|
+
name: '기본 Claw (빨강)',
|
|
50
|
+
colorMap: { primary: '#ff4f40', secondary: '#ff775f', dark: '#8B4513', eye: '#ffffff', pupil: '#111111', claw: '#ff4f40' },
|
|
51
|
+
},
|
|
52
|
+
blue: {
|
|
53
|
+
name: '파란 Claw',
|
|
54
|
+
colorMap: { primary: '#4488ff', secondary: '#6699ff', dark: '#223388', eye: '#ffffff', pupil: '#111111', claw: '#4488ff' },
|
|
55
|
+
},
|
|
56
|
+
green: {
|
|
57
|
+
name: '초록 Claw',
|
|
58
|
+
colorMap: { primary: '#44cc44', secondary: '#66dd66', dark: '#226622', eye: '#ffffff', pupil: '#111111', claw: '#44cc44' },
|
|
59
|
+
},
|
|
60
|
+
purple: {
|
|
61
|
+
name: '보라 Claw',
|
|
62
|
+
colorMap: { primary: '#8844cc', secondary: '#aa66dd', dark: '#442266', eye: '#ffffff', pupil: '#111111', claw: '#8844cc' },
|
|
63
|
+
},
|
|
64
|
+
gold: {
|
|
65
|
+
name: '골드 Claw',
|
|
66
|
+
colorMap: { primary: '#ffcc00', secondary: '#ffdd44', dark: '#886600', eye: '#ffffff', pupil: '#111111', claw: '#ffcc00' },
|
|
67
|
+
},
|
|
68
|
+
pink: {
|
|
69
|
+
name: '핑크 Claw',
|
|
70
|
+
colorMap: { primary: '#ff69b4', secondary: '#ff8cc4', dark: '#8B3060', eye: '#ffffff', pupil: '#111111', claw: '#ff69b4' },
|
|
71
|
+
},
|
|
72
|
+
cat: {
|
|
73
|
+
name: '고양이',
|
|
74
|
+
colorMap: { primary: '#ff9944', secondary: '#ffbb66', dark: '#663300', eye: '#88ff88', pupil: '#111111', claw: '#ff9944' },
|
|
75
|
+
},
|
|
76
|
+
robot: {
|
|
77
|
+
name: '로봇',
|
|
78
|
+
colorMap: { primary: '#888888', secondary: '#aaaaaa', dark: '#444444', eye: '#66aaff', pupil: '#0044aa', claw: '#66aaff' },
|
|
79
|
+
},
|
|
80
|
+
ghost: {
|
|
81
|
+
name: '유령',
|
|
82
|
+
colorMap: { primary: '#ccccff', secondary: '#eeeeff', dark: '#6666aa', eye: '#ff6666', pupil: '#cc0000', claw: '#ccccff' },
|
|
83
|
+
},
|
|
84
|
+
dragon: {
|
|
85
|
+
name: '드래곤',
|
|
86
|
+
colorMap: { primary: '#cc2222', secondary: '#ff4444', dark: '#661111', eye: '#ffaa00', pupil: '#111111', claw: '#ffaa00' },
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
56
90
|
function createClawIcon() {
|
|
57
91
|
const size = 16;
|
|
58
92
|
const buffer = Buffer.alloc(size * size * 4);
|
|
59
|
-
|
|
60
93
|
for (let y = 0; y < size; y++) {
|
|
61
94
|
for (let x = 0; x < size; x++) {
|
|
62
95
|
const code = CLAW_ICON[y][x];
|
|
63
96
|
const color = COLOR_MAP[code] || COLOR_MAP[0];
|
|
64
97
|
const offset = (y * size + x) * 4;
|
|
65
|
-
buffer[offset + 0] = color[0];
|
|
66
|
-
buffer[offset + 1] = color[1];
|
|
67
|
-
buffer[offset + 2] = color[2];
|
|
68
|
-
buffer[offset + 3] = color[3];
|
|
98
|
+
buffer[offset + 0] = color[0];
|
|
99
|
+
buffer[offset + 1] = color[1];
|
|
100
|
+
buffer[offset + 2] = color[2];
|
|
101
|
+
buffer[offset + 3] = color[3];
|
|
69
102
|
}
|
|
70
103
|
}
|
|
71
|
-
|
|
72
104
|
return nativeImage.createFromBuffer(buffer, { width: size, height: size });
|
|
73
105
|
}
|
|
74
106
|
|
|
75
107
|
function setupTray(mainWindow, bridge) {
|
|
76
108
|
aiBridge = bridge;
|
|
77
|
-
const store = new Store('clawmate-config', {
|
|
109
|
+
const store = new Store('clawmate-config', {
|
|
110
|
+
mode: 'pet',
|
|
111
|
+
character: 'default',
|
|
112
|
+
telegramToken: '',
|
|
113
|
+
});
|
|
78
114
|
|
|
79
|
-
// Claw 픽셀아트 트레이 아이콘 생성
|
|
80
115
|
const icon = createClawIcon();
|
|
81
|
-
|
|
82
116
|
tray = new Tray(icon);
|
|
83
117
|
tray.setToolTip('ClawMate - 데스크톱 펫');
|
|
84
118
|
|
|
@@ -87,38 +121,94 @@ function setupTray(mainWindow, bridge) {
|
|
|
87
121
|
const fileInteraction = store.get('fileInteraction') !== false;
|
|
88
122
|
const aiConnected = aiBridge ? aiBridge.isConnected() : false;
|
|
89
123
|
const autoStart = isAutoStartEnabled();
|
|
124
|
+
const currentChar = store.get('character') || 'default';
|
|
125
|
+
const hasTelegramToken = !!(store.get('telegramToken'));
|
|
126
|
+
|
|
127
|
+
// 캐릭터 서브메뉴
|
|
128
|
+
const characterSubmenu = Object.entries(CHARACTER_PRESETS).map(([key, preset]) => ({
|
|
129
|
+
label: preset.name,
|
|
130
|
+
type: 'radio',
|
|
131
|
+
checked: currentChar === key,
|
|
132
|
+
click: () => {
|
|
133
|
+
store.set('character', key);
|
|
134
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
135
|
+
if (key === 'default') {
|
|
136
|
+
mainWindow.webContents.send('ai-command', {
|
|
137
|
+
type: 'reset_character', payload: {},
|
|
138
|
+
});
|
|
139
|
+
} else {
|
|
140
|
+
mainWindow.webContents.send('ai-command', {
|
|
141
|
+
type: 'set_character', payload: {
|
|
142
|
+
colorMap: preset.colorMap,
|
|
143
|
+
speech: `${preset.name}(으)로 변신!`,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
buildAndSet();
|
|
149
|
+
},
|
|
150
|
+
}));
|
|
90
151
|
|
|
91
152
|
return Menu.buildFromTemplate([
|
|
92
153
|
{
|
|
93
|
-
label: `ClawMate (${mode === 'pet' ? 'Clawby' : '
|
|
154
|
+
label: `ClawMate (${mode === 'pet' ? 'Clawby' : mode === 'incarnation' ? 'Claw' : 'Clawby + Claw'})`,
|
|
94
155
|
enabled: false,
|
|
95
156
|
},
|
|
96
157
|
{
|
|
97
|
-
label: aiConnected ? 'AI: 연결됨' : 'AI: 자율 모드
|
|
158
|
+
label: aiConnected ? 'AI: 연결됨' : 'AI: 자율 모드',
|
|
98
159
|
enabled: false,
|
|
99
160
|
},
|
|
100
161
|
{ type: 'separator' },
|
|
162
|
+
|
|
163
|
+
// === 모드 선택 ===
|
|
101
164
|
{
|
|
102
|
-
label: '
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
165
|
+
label: '모드',
|
|
166
|
+
submenu: [
|
|
167
|
+
{
|
|
168
|
+
label: 'Pet 모드 (Clawby)',
|
|
169
|
+
sublabel: '귀여운 펫을 키우기',
|
|
170
|
+
type: 'radio',
|
|
171
|
+
checked: mode === 'pet',
|
|
172
|
+
click: () => {
|
|
173
|
+
store.set('mode', 'pet');
|
|
174
|
+
if (mainWindow) mainWindow.webContents.send('mode-changed', 'pet');
|
|
175
|
+
buildAndSet();
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
label: 'Incarnation 모드 (Claw)',
|
|
180
|
+
sublabel: '봇이 육체를 얻음',
|
|
181
|
+
type: 'radio',
|
|
182
|
+
checked: mode === 'incarnation',
|
|
183
|
+
click: () => {
|
|
184
|
+
store.set('mode', 'incarnation');
|
|
185
|
+
if (mainWindow) mainWindow.webContents.send('mode-changed', 'incarnation');
|
|
186
|
+
buildAndSet();
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
label: '둘 다 (Pet + Incarnation)',
|
|
191
|
+
sublabel: '펫도 키우고, 봇 인격도 반영',
|
|
192
|
+
type: 'radio',
|
|
193
|
+
checked: mode === 'both',
|
|
194
|
+
click: () => {
|
|
195
|
+
store.set('mode', 'both');
|
|
196
|
+
if (mainWindow) mainWindow.webContents.send('mode-changed', 'both');
|
|
197
|
+
buildAndSet();
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
],
|
|
110
201
|
},
|
|
202
|
+
|
|
203
|
+
// === 캐릭터 선택 ===
|
|
111
204
|
{
|
|
112
|
-
label: '
|
|
113
|
-
|
|
114
|
-
checked: mode === 'incarnation',
|
|
115
|
-
click: () => {
|
|
116
|
-
store.set('mode', 'incarnation');
|
|
117
|
-
if (mainWindow) mainWindow.webContents.send('mode-changed', 'incarnation');
|
|
118
|
-
buildAndSet();
|
|
119
|
-
},
|
|
205
|
+
label: '캐릭터',
|
|
206
|
+
submenu: characterSubmenu,
|
|
120
207
|
},
|
|
208
|
+
|
|
121
209
|
{ type: 'separator' },
|
|
210
|
+
|
|
211
|
+
// === 설정 ===
|
|
122
212
|
{
|
|
123
213
|
label: '파일 상호작용',
|
|
124
214
|
type: 'checkbox',
|
|
@@ -137,7 +227,83 @@ function setupTray(mainWindow, bridge) {
|
|
|
137
227
|
buildAndSet();
|
|
138
228
|
},
|
|
139
229
|
},
|
|
230
|
+
|
|
140
231
|
{ type: 'separator' },
|
|
232
|
+
|
|
233
|
+
// === 텔레그램 봇 ===
|
|
234
|
+
{
|
|
235
|
+
label: '텔레그램 봇',
|
|
236
|
+
submenu: [
|
|
237
|
+
{
|
|
238
|
+
label: hasTelegramToken ? '봇 토큰: 설정됨' : '봇 토큰: 미설정',
|
|
239
|
+
enabled: false,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
label: '봇 토큰 설정...',
|
|
243
|
+
click: async () => {
|
|
244
|
+
const result = await dialog.showMessageBox({
|
|
245
|
+
type: 'question',
|
|
246
|
+
buttons: ['클립보드에서 붙여넣기', '직접 입력', '취소'],
|
|
247
|
+
title: 'ClawMate 텔레그램 봇',
|
|
248
|
+
message: '텔레그램 봇 토큰을 설정합니다.',
|
|
249
|
+
detail: '@BotFather에서 받은 봇 토큰을 입력하세요.\n현재 클립보드 내용을 붙여넣으려면 "클립보드에서 붙여넣기"를 선택하세요.',
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
let token = null;
|
|
253
|
+
if (result.response === 0) {
|
|
254
|
+
// 클립보드에서 붙여넣기
|
|
255
|
+
token = clipboard.readText().trim();
|
|
256
|
+
} else if (result.response === 1) {
|
|
257
|
+
// prompt가 없으므로 클립보드 안내
|
|
258
|
+
const promptResult = await dialog.showMessageBox({
|
|
259
|
+
type: 'info',
|
|
260
|
+
buttons: ['확인'],
|
|
261
|
+
title: '텔레그램 봇 토큰',
|
|
262
|
+
message: '봇 토큰을 클립보드에 복사한 후 다시 "봇 토큰 설정"을 선택하세요.',
|
|
263
|
+
detail: '텔레그램에서 @BotFather → /newbot → 토큰 복사',
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
} else {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (token && token.includes(':')) {
|
|
271
|
+
store.set('telegramToken', token);
|
|
272
|
+
process.env.CLAWMATE_TELEGRAM_TOKEN = token;
|
|
273
|
+
buildAndSet();
|
|
274
|
+
|
|
275
|
+
// 펫 알림
|
|
276
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
277
|
+
mainWindow.webContents.send('ai-command', {
|
|
278
|
+
type: 'speak',
|
|
279
|
+
payload: { text: '텔레그램 봇 토큰 설정 완료!' },
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
await dialog.showMessageBox({
|
|
284
|
+
type: 'error',
|
|
285
|
+
buttons: ['확인'],
|
|
286
|
+
title: '잘못된 토큰',
|
|
287
|
+
message: '유효한 텔레그램 봇 토큰이 아닙니다.',
|
|
288
|
+
detail: '올바른 형식: 123456789:ABCdefGHIjklMNOpqrsTUVwxyz',
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
label: '봇 토큰 제거',
|
|
295
|
+
enabled: hasTelegramToken,
|
|
296
|
+
click: () => {
|
|
297
|
+
store.set('telegramToken', '');
|
|
298
|
+
delete process.env.CLAWMATE_TELEGRAM_TOKEN;
|
|
299
|
+
buildAndSet();
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
{ type: 'separator' },
|
|
306
|
+
|
|
141
307
|
{
|
|
142
308
|
label: '업데이트 확인',
|
|
143
309
|
click: async () => {
|
|
@@ -178,13 +344,10 @@ function setupTray(mainWindow, bridge) {
|
|
|
178
344
|
}
|
|
179
345
|
|
|
180
346
|
/**
|
|
181
|
-
* 수동 업데이트 확인
|
|
182
|
-
* 빌드된 앱: electron-updater 사용
|
|
183
|
-
* npm 설치: npm registry에서 최신 버전 비교
|
|
347
|
+
* 수동 업데이트 확인
|
|
184
348
|
*/
|
|
185
349
|
async function checkForUpdateManual(mainWindow) {
|
|
186
350
|
if (app.isPackaged) {
|
|
187
|
-
// electron-updater 기반 업데이트
|
|
188
351
|
try {
|
|
189
352
|
const { autoUpdater } = require('electron-updater');
|
|
190
353
|
autoUpdater.checkForUpdatesAndNotify();
|
|
@@ -192,7 +355,6 @@ async function checkForUpdateManual(mainWindow) {
|
|
|
192
355
|
console.error('[업데이트] electron-updater 실패:', err.message);
|
|
193
356
|
}
|
|
194
357
|
} else {
|
|
195
|
-
// npm 기반 업데이트 확인
|
|
196
358
|
try {
|
|
197
359
|
const latest = execSync('npm view clawmate version', {
|
|
198
360
|
encoding: 'utf-8',
|
|
@@ -201,34 +363,27 @@ async function checkForUpdateManual(mainWindow) {
|
|
|
201
363
|
const current = require('../package.json').version;
|
|
202
364
|
|
|
203
365
|
if (latest !== current) {
|
|
204
|
-
// 펫 말풍선으로 알림
|
|
205
366
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
206
367
|
mainWindow.webContents.send('ai-command', {
|
|
207
368
|
type: 'speak',
|
|
208
369
|
payload: { text: `새 버전 v${latest} 사용 가능! (현재: v${current})` },
|
|
209
370
|
});
|
|
210
371
|
}
|
|
211
|
-
console.log(`[업데이트] 새 버전 ${latest} 사용 가능 (현재: ${current})`);
|
|
212
|
-
console.log('[업데이트] npm update -g clawmate');
|
|
213
|
-
|
|
214
|
-
// npm 페이지 열기
|
|
215
372
|
shell.openExternal('https://www.npmjs.com/package/clawmate');
|
|
216
373
|
} else {
|
|
217
|
-
// 이미 최신
|
|
218
374
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
219
375
|
mainWindow.webContents.send('ai-command', {
|
|
220
376
|
type: 'speak',
|
|
221
377
|
payload: { text: `v${current} — 이미 최신 버전이야!` },
|
|
222
378
|
});
|
|
223
379
|
}
|
|
224
|
-
console.log(`[업데이트] 현재 최신 버전 (v${current})`);
|
|
225
380
|
}
|
|
226
381
|
} catch (err) {
|
|
227
382
|
console.error('[업데이트] npm 버전 확인 실패:', err.message);
|
|
228
383
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
229
384
|
mainWindow.webContents.send('ai-command', {
|
|
230
385
|
type: 'speak',
|
|
231
|
-
payload: { text: '업데이트 확인 실패...
|
|
386
|
+
payload: { text: '업데이트 확인 실패...' },
|
|
232
387
|
});
|
|
233
388
|
}
|
|
234
389
|
}
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawmate",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "ClawMate - AI가 조종하는 화면 위의 살아있는 데스크톱 펫",
|
|
5
5
|
"main": "main/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"clawmate": "./skills/launch-pet/index.js"
|
|
@@ -18,13 +18,12 @@
|
|
|
18
18
|
"build:linux": "electron-builder --linux"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
|
-
"openclaw",
|
|
22
21
|
"desktop-pet",
|
|
23
22
|
"electron",
|
|
24
23
|
"clawmate",
|
|
25
24
|
"ai-pet"
|
|
26
25
|
],
|
|
27
|
-
"author": "
|
|
26
|
+
"author": "ClawMate",
|
|
28
27
|
"license": "MIT",
|
|
29
28
|
"homepage": "https://github.com/boqum/clawmate",
|
|
30
29
|
"dependencies": {
|
package/preload/preload.js
CHANGED
|
@@ -42,9 +42,9 @@ contextBridge.exposeInMainWorld('clawmate', {
|
|
|
42
42
|
ipcRenderer.on('config-changed', (_, config) => callback(config));
|
|
43
43
|
},
|
|
44
44
|
|
|
45
|
-
// ===
|
|
45
|
+
// === AI 통신 ===
|
|
46
46
|
|
|
47
|
-
// AI 명령 수신 (
|
|
47
|
+
// AI 명령 수신 (AI → 펫)
|
|
48
48
|
onAICommand: (callback) => {
|
|
49
49
|
ipcRenderer.on('ai-command', (_, command) => callback(command));
|
|
50
50
|
},
|
|
@@ -57,13 +57,13 @@ contextBridge.exposeInMainWorld('clawmate', {
|
|
|
57
57
|
ipcRenderer.on('ai-disconnected', () => callback());
|
|
58
58
|
},
|
|
59
59
|
|
|
60
|
-
// 사용자 이벤트를
|
|
60
|
+
// 사용자 이벤트를 AI에 전달 (펫 → AI)
|
|
61
61
|
reportToAI: (event, data) => ipcRenderer.send('report-to-ai', event, data),
|
|
62
62
|
|
|
63
63
|
// AI 연결 상태 확인
|
|
64
64
|
isAIConnected: () => ipcRenderer.invoke('is-ai-connected'),
|
|
65
65
|
|
|
66
|
-
// 메트릭 보고 (렌더러 → main →
|
|
66
|
+
// 메트릭 보고 (렌더러 → main → AI)
|
|
67
67
|
reportMetrics: (summary) => ipcRenderer.send('report-metrics', summary),
|
|
68
68
|
|
|
69
69
|
// 활성 윈도우 제목 조회 (브라우저 감시)
|
package/renderer/first-run.html
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
<div class="mode-card" id="mode-incarnation" onclick="selectMode('incarnation')">
|
|
50
50
|
<div class="mode-icon">\u{1F980}</div>
|
|
51
|
-
<h2>
|
|
51
|
+
<h2>Claw</h2>
|
|
52
52
|
<p>육체를 얻은 존재</p>
|
|
53
53
|
<span class="mode-desc">침착하고 카리스마 있는 Claw!</span>
|
|
54
54
|
</div>
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
document.getElementById('mode-' + mode).classList.add('selected');
|
|
73
73
|
const btn = document.getElementById('start-btn');
|
|
74
74
|
btn.disabled = false;
|
|
75
|
-
btn.textContent = mode === 'pet' ? 'Clawby와 시작!' : '
|
|
75
|
+
btn.textContent = mode === 'pet' ? 'Clawby와 시작!' : 'Claw과 시작!';
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
async function startFirstRun() {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI 행동 컨트롤러
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* AI가 연결되면 → AI가 모든 행동을 결정
|
|
5
|
+
* AI가 끊기면 → 자율 모드 (기존 FSM) 로 폴백
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* AI가 결정하는 것:
|
|
8
8
|
* - 언제 뭐라고 말할지
|
|
9
9
|
* - 어디로 움직일지
|
|
10
10
|
* - 어떤 감정을 표현할지
|
|
@@ -32,7 +32,7 @@ const AIController = (() => {
|
|
|
32
32
|
window.clawmate.onAIConnected(() => {
|
|
33
33
|
connected = true;
|
|
34
34
|
autonomousMode = false;
|
|
35
|
-
Speech.show('
|
|
35
|
+
Speech.show('AI 연결됨... 의식이 깨어난다.');
|
|
36
36
|
StateMachine.forceState('excited');
|
|
37
37
|
});
|
|
38
38
|
}
|
|
@@ -47,7 +47,7 @@ const AIController = (() => {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
50
|
+
* AI로부터 온 명령 실행
|
|
51
51
|
*/
|
|
52
52
|
function handleAICommand(command) {
|
|
53
53
|
const { type, payload } = command;
|
|
@@ -144,7 +144,7 @@ const AIController = (() => {
|
|
|
144
144
|
// === 커스텀 이동 패턴 ===
|
|
145
145
|
|
|
146
146
|
case 'register_movement':
|
|
147
|
-
//
|
|
147
|
+
// AI가 JSON으로 이동 패턴 정의를 보내면 등록
|
|
148
148
|
// payload: { name, definition }
|
|
149
149
|
// definition: { type, params } — 각 타입별 파라미터
|
|
150
150
|
_registerAIMovement(payload.name, payload.definition);
|
|
@@ -200,6 +200,17 @@ const AIController = (() => {
|
|
|
200
200
|
StateMachine.forceState('excited');
|
|
201
201
|
break;
|
|
202
202
|
|
|
203
|
+
// === 인격체 전환 (Incarnation 모드) ===
|
|
204
|
+
case 'set_persona':
|
|
205
|
+
// 봇 인격체 데이터 적용
|
|
206
|
+
if (typeof ModeManager !== 'undefined') {
|
|
207
|
+
ModeManager.setPersona(payload);
|
|
208
|
+
const name = payload.name || 'Claw';
|
|
209
|
+
Speech.show(`${name}의 인격이 깨어났다.`);
|
|
210
|
+
StateMachine.forceState('excited');
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
|
|
203
214
|
// === 스마트 파일 조작 애니메이션 ===
|
|
204
215
|
case 'smart_file_op':
|
|
205
216
|
handleSmartFileOp(payload);
|
|
@@ -290,7 +301,7 @@ const AIController = (() => {
|
|
|
290
301
|
}
|
|
291
302
|
|
|
292
303
|
/**
|
|
293
|
-
*
|
|
304
|
+
* AI가 JSON으로 정의한 이동 패턴을 동적으로 등록
|
|
294
305
|
* 안전한 실행을 위해 Function 생성자 대신 사전정의된 행동 유형 조합 사용
|
|
295
306
|
*
|
|
296
307
|
* definition 형식:
|
|
@@ -439,7 +450,7 @@ const AIController = (() => {
|
|
|
439
450
|
|
|
440
451
|
/**
|
|
441
452
|
* AI 종합 의사결정 실행
|
|
442
|
-
*
|
|
453
|
+
* AI가 상황을 분석하고 내린 복합적 결정
|
|
443
454
|
*
|
|
444
455
|
* 예시:
|
|
445
456
|
* {
|
|
@@ -496,7 +507,7 @@ const AIController = (() => {
|
|
|
496
507
|
StateMachine.forceState(state);
|
|
497
508
|
}
|
|
498
509
|
|
|
499
|
-
// === 사용자 이벤트 →
|
|
510
|
+
// === 사용자 이벤트 → AI에 리포트 ===
|
|
500
511
|
|
|
501
512
|
function reportClick(position) {
|
|
502
513
|
if (window.clawmate.reportToAI) {
|