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.
@@ -0,0 +1,172 @@
1
+ /**
2
+ * 브라우저 활동 감시 + AI 코멘트 시스템
3
+ *
4
+ * 두 가지 모드:
5
+ * AI 연결 시: 윈도우 제목 + 화면 캡처 + 커서 위치를 AI에 전송 → AI가 맥락 있는 코멘트 생성
6
+ * AI 미연결 시: 프리셋 메시지로 폴백 (자율 모드)
7
+ *
8
+ * 동작:
9
+ * 1. 15초마다 활성 윈도우 제목 + 커서 위치 조회
10
+ * 2. 브라우저/앱 감지 시 AI에 컨텍스트 리포트 (제목 + 화면 캡처)
11
+ * 3. AI가 제목/캡처를 분석해서 상황 맞는 코멘트 생성
12
+ * 4. 자율 모드에서는 프리셋 메시지 폴백
13
+ */
14
+ const BrowserWatcher = (() => {
15
+ const CHECK_INTERVAL = 15000; // 활성 윈도우 체크 주기 (15초)
16
+ const AI_COOLDOWN = 45000; // AI 코멘트 쿨다운 (45초)
17
+ const FALLBACK_COOLDOWN = 90000; // 자율 모드 코멘트 쿨다운 (90초)
18
+ const COMMENT_CHANCE = 0.4; // 코멘트 확률 (40%)
19
+ const SITE_CHANGE_BONUS = 0.3; // 사이트 변경 시 추가 확률
20
+
21
+ let intervalId = null;
22
+ let lastCategory = null;
23
+ let lastCommentTime = 0;
24
+ let lastTitle = '';
25
+ let enabled = true;
26
+
27
+ function init() {
28
+ intervalId = setInterval(check, CHECK_INTERVAL);
29
+ // 첫 체크는 10초 후 (앱 시작 직후는 건너뜀)
30
+ setTimeout(check, 10000);
31
+ }
32
+
33
+ async function check() {
34
+ if (!enabled) return;
35
+ if (typeof Speech === 'undefined') return;
36
+
37
+ // sleeping 상태면 참견 안 함
38
+ if (typeof StateMachine !== 'undefined' && StateMachine.getState() === 'sleeping') return;
39
+
40
+ try {
41
+ const title = await window.clawmate.getActiveWindowTitle();
42
+ if (!title) return;
43
+
44
+ const titleLower = title.toLowerCase();
45
+ const titleChanged = title !== lastTitle;
46
+ lastTitle = title;
47
+
48
+ const now = Date.now();
49
+
50
+ // 카테고리 매칭 (AI/자율 모두 사용)
51
+ const msgs = window._messages;
52
+ const match = msgs?.browsing ? findCategory(titleLower, msgs.browsing) : null;
53
+ const category = match?.category || 'unknown';
54
+
55
+ // 쿨다운 체크
56
+ const isAI = typeof AIController !== 'undefined' && AIController.isConnected();
57
+ const cooldown = isAI ? AI_COOLDOWN : FALLBACK_COOLDOWN;
58
+ if (now - lastCommentTime < cooldown) return;
59
+
60
+ // 같은 카테고리 + 제목 미변경 시 스킵
61
+ if (category === lastCategory && !titleChanged) return;
62
+
63
+ // 확률 체크
64
+ let chance = COMMENT_CHANCE;
65
+ if (titleChanged) chance += SITE_CHANGE_BONUS;
66
+ if (Math.random() > chance) return;
67
+
68
+ // === AI vs 자율 모드 분기 ===
69
+ if (isAI) {
70
+ await reportBrowsingToAI(title, category, titleChanged);
71
+ } else {
72
+ showFallbackComment(match);
73
+ }
74
+
75
+ lastCategory = category;
76
+ lastCommentTime = now;
77
+ } catch {
78
+ // IPC 실패 무시
79
+ }
80
+ }
81
+
82
+ /**
83
+ * AI에 브라우징 컨텍스트 전송
84
+ * 제목 + 커서 위치 + 화면 캡처를 한번에 전송
85
+ * AI(OpenClaw 플러그인)가 분석하고 코멘트 생성
86
+ */
87
+ async function reportBrowsingToAI(title, category, titleChanged) {
88
+ if (!window.clawmate.reportToAI) return;
89
+
90
+ // 커서 위치 조회
91
+ let cursorX = 0, cursorY = 0;
92
+ try {
93
+ if (window.clawmate.getCursorPosition) {
94
+ const pos = await window.clawmate.getCursorPosition();
95
+ cursorX = pos.x;
96
+ cursorY = pos.y;
97
+ }
98
+ } catch {}
99
+
100
+ // 화면 캡처 (AI가 페이지 내용을 시각적으로 분석하기 위해)
101
+ let screenData = null;
102
+ try {
103
+ const capture = await window.clawmate.screen.capture();
104
+ if (capture?.success) {
105
+ screenData = {
106
+ image: capture.image,
107
+ width: capture.width,
108
+ height: capture.height,
109
+ };
110
+ }
111
+ } catch {}
112
+
113
+ // 통합 브라우징 리포트 전송
114
+ window.clawmate.reportToAI('browsing', {
115
+ title,
116
+ category,
117
+ titleChanged,
118
+ cursorX,
119
+ cursorY,
120
+ screen: screenData,
121
+ timestamp: Date.now(),
122
+ });
123
+ }
124
+
125
+ /**
126
+ * 자율 모드 폴백: 프리셋 메시지 표시
127
+ */
128
+ function showFallbackComment(match) {
129
+ if (!match?.data?.comments) return;
130
+
131
+ const comments = match.data.comments;
132
+ const comment = comments[Math.floor(Math.random() * comments.length)];
133
+ Speech.show(comment);
134
+
135
+ // 50% 확률로 흥분 애니메이션
136
+ if (typeof StateMachine !== 'undefined') {
137
+ const state = StateMachine.getState();
138
+ if ((state === 'idle' || state === 'walking') && Math.random() < 0.5) {
139
+ StateMachine.forceState('excited');
140
+ setTimeout(() => {
141
+ if (StateMachine.getState() === 'excited') StateMachine.forceState('idle');
142
+ }, 1500);
143
+ }
144
+ }
145
+ }
146
+
147
+ /**
148
+ * 카테고리 매칭 (키워드 기반)
149
+ * general은 다른 카테고리 매칭 안 될 때만 사용
150
+ */
151
+ function findCategory(titleLower, browsingMsgs) {
152
+ let generalMatch = null;
153
+ for (const [category, data] of Object.entries(browsingMsgs)) {
154
+ if (!data.keywords) continue;
155
+ for (const keyword of data.keywords) {
156
+ if (titleLower.includes(keyword.toLowerCase())) {
157
+ if (category === 'general') {
158
+ generalMatch = { category, data };
159
+ } else {
160
+ return { category, data };
161
+ }
162
+ }
163
+ }
164
+ }
165
+ return generalMatch;
166
+ }
167
+
168
+ function setEnabled(val) { enabled = val; }
169
+ function stop() { if (intervalId) clearInterval(intervalId); }
170
+
171
+ return { init, stop, setEnabled, check };
172
+ })();
@@ -55,7 +55,7 @@ const Character = (() => {
55
55
  ],
56
56
 
57
57
  walk: [
58
- // Frame 0: 왼발
58
+ // Frame 0: 왼쪽 다리 세트 앞으로 크게 벌림, 집게 열림
59
59
  [
60
60
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
61
61
  [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
@@ -70,17 +70,17 @@ const Character = (() => {
70
70
  [0,0,1,2,1,1,1,1,1,1,1,1,2,1,0,0],
71
71
  [0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0],
72
72
  [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
73
- [0,0,3,3,3,0,3,3,3,3,0,0,3,3,3,0],
74
- [0,3,3,3,0,0,0,3,3,0,0,0,3,3,3,0],
73
+ [0,3,3,0,0,0,3,3,3,3,0,0,0,3,3,0],
74
+ [3,3,0,0,0,0,0,3,3,0,0,0,0,0,3,3],
75
75
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
76
76
  ],
77
- // Frame 1: 오른발
77
+ // Frame 1: 양쪽 다리 모임 (접촉 순간), 집게 반 닫힘
78
78
  [
79
79
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
80
80
  [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
81
- [0,6,0,0,6,0,0,0,0,0,0,6,0,0,6,0],
82
- [0,6,0,0,6,0,0,0,0,0,0,6,0,0,6,0],
83
- [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
81
+ [0,0,6,0,6,0,0,0,0,0,0,6,0,6,0,0],
82
+ [0,0,6,0,6,0,0,0,0,0,0,6,0,6,0,0],
83
+ [0,0,0,6,6,0,0,0,0,0,0,0,6,6,0,0],
84
84
  [0,0,0,6,1,1,0,0,0,0,1,1,6,0,0,0],
85
85
  [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
86
86
  [0,0,0,1,1,4,5,1,1,4,5,1,1,0,0,0],
@@ -89,11 +89,11 @@ const Character = (() => {
89
89
  [0,0,1,2,1,1,1,1,1,1,1,1,2,1,0,0],
90
90
  [0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0],
91
91
  [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
92
- [0,0,0,3,3,3,0,3,3,0,3,3,3,0,0,0],
93
- [0,0,0,0,3,3,3,0,0,3,3,3,0,0,0,0],
92
+ [0,0,0,3,3,0,3,3,3,3,0,3,3,0,0,0],
93
+ [0,0,0,3,3,0,0,3,3,0,0,3,3,0,0,0],
94
94
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
95
95
  ],
96
- // Frame 2: 왼발
96
+ // Frame 2: 오른쪽 다리 세트 앞으로 크게 벌림, 집게 닫힘
97
97
  [
98
98
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
99
99
  [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
@@ -108,17 +108,17 @@ const Character = (() => {
108
108
  [0,0,1,2,1,1,1,1,1,1,1,1,2,1,0,0],
109
109
  [0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0],
110
110
  [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
111
- [0,0,3,3,3,0,3,3,3,3,0,0,3,3,3,0],
112
- [0,3,3,3,0,0,0,3,3,0,0,0,3,3,3,0],
111
+ [0,0,3,3,0,0,3,3,3,3,0,0,3,3,0,0],
112
+ [0,3,3,0,0,0,0,3,3,0,0,0,0,3,3,0],
113
113
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
114
114
  ],
115
- // Frame 3: 오른발
115
+ // Frame 3: 양쪽 다리 모임 (접촉 순간), 집게 반 열림
116
116
  [
117
117
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
118
118
  [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
119
+ [0,6,0,0,6,0,0,0,0,0,0,6,0,0,6,0],
120
+ [0,6,0,0,6,0,0,0,0,0,0,6,0,0,6,0],
119
121
  [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
120
- [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
121
- [0,0,0,6,6,0,0,0,0,0,0,6,6,0,0,0],
122
122
  [0,0,0,6,1,1,0,0,0,0,1,1,6,0,0,0],
123
123
  [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
124
124
  [0,0,0,1,1,4,5,1,1,4,5,1,1,0,0,0],
@@ -127,14 +127,14 @@ const Character = (() => {
127
127
  [0,0,1,2,1,1,1,1,1,1,1,1,2,1,0,0],
128
128
  [0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0],
129
129
  [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
130
- [0,0,0,3,3,3,0,3,3,0,3,3,3,0,0,0],
131
- [0,0,0,0,3,3,3,0,0,3,3,3,0,0,0,0],
130
+ [0,0,0,3,3,0,3,3,3,3,0,3,3,0,0,0],
131
+ [0,0,0,3,3,0,0,3,3,0,0,3,3,0,0,0],
132
132
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
133
133
  ],
134
134
  ],
135
135
 
136
136
  climb: [
137
- // Frame 0: 기어오르기 포즈 1 (옆모습)
137
+ // Frame 0: 기어오르기 윗다리 세트 뻗음, 집게 열림
138
138
  [
139
139
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
140
140
  [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
@@ -147,13 +147,13 @@ const Character = (() => {
147
147
  [0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0],
148
148
  [0,0,0,1,2,1,1,1,1,0,0,0,0,0,0,0],
149
149
  [0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0],
150
- [0,0,0,3,3,0,3,3,0,0,0,0,0,0,0,0],
151
150
  [0,0,3,3,0,0,0,3,3,0,0,0,0,0,0,0],
151
+ [0,3,3,0,0,0,0,0,3,3,0,0,0,0,0,0],
152
152
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
153
153
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
154
154
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
155
155
  ],
156
- // Frame 1: 기어오르기 포즈 2
156
+ // Frame 1: 기어오르기 다리 모임 (교차 순간)
157
157
  [
158
158
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
159
159
  [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
@@ -166,12 +166,50 @@ const Character = (() => {
166
166
  [0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0],
167
167
  [0,0,0,1,2,1,1,1,1,0,0,0,0,0,0,0],
168
168
  [0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0],
169
- [0,0,3,3,0,0,0,3,3,0,0,0,0,0,0,0],
169
+ [0,0,0,3,3,0,3,3,0,0,0,0,0,0,0,0],
170
170
  [0,0,0,3,3,0,3,3,0,0,0,0,0,0,0,0],
171
171
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
172
172
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
173
173
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
174
174
  ],
175
+ // Frame 2: 기어오르기 — 아랫다리 세트 뻗음, 집게 닫힘
176
+ [
177
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
178
+ [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
179
+ [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
180
+ [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
181
+ [0,0,0,0,6,6,1,1,0,0,0,0,0,0,0,0],
182
+ [0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0],
183
+ [0,0,0,0,1,4,5,1,1,0,0,0,0,0,0,0],
184
+ [0,0,0,0,1,4,5,1,1,1,0,0,0,0,0,0],
185
+ [0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0],
186
+ [0,0,0,1,2,1,1,1,1,0,0,0,0,0,0,0],
187
+ [0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0],
188
+ [0,0,0,3,3,0,3,3,0,0,0,0,0,0,0,0],
189
+ [0,0,3,3,0,0,0,3,3,0,0,0,0,0,0,0],
190
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
191
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
192
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
193
+ ],
194
+ // Frame 3: 기어오르기 — 다리 모임 (교차 순간, 반대쪽)
195
+ [
196
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
197
+ [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
198
+ [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
199
+ [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
200
+ [0,0,0,0,6,6,1,1,0,0,0,0,0,0,0,0],
201
+ [0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0],
202
+ [0,0,0,0,1,4,5,1,1,0,0,0,0,0,0,0],
203
+ [0,0,0,0,1,4,5,1,1,1,0,0,0,0,0,0],
204
+ [0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0],
205
+ [0,0,0,1,2,1,1,1,1,0,0,0,0,0,0,0],
206
+ [0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0],
207
+ [0,0,3,3,0,0,0,3,3,0,0,0,0,0,0,0],
208
+ [0,0,3,3,0,0,0,3,3,0,0,0,0,0,0,0],
209
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
210
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
211
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
212
+ ],
175
213
  ],
176
214
 
177
215
  sleep: [
@@ -335,10 +373,12 @@ const Character = (() => {
335
373
  jumping: 'excited', // 점프: excited 프레임셋 재활용
336
374
  rappelling: 'climb', // 레펠: climb 프레임셋 재활용
337
375
  falling: 'scared', // 낙하: scared 프레임셋 재활용
376
+ custom: 'walk', // 커스텀 이동: 기본은 walk, 패턴별로 동적 변경 가능
338
377
  };
339
378
 
340
379
  let currentCanvas = null;
341
380
  let currentColorMap = null;
381
+ let originalFrames = null; // 원본 프레임 백업 (리셋용)
342
382
 
343
383
  function createCanvas(container) {
344
384
  const canvas = document.createElement('canvas');
@@ -395,5 +435,62 @@ const Character = (() => {
395
435
  return (FRAMES[frameSet] || FRAMES.idle).length;
396
436
  }
397
437
 
398
- return { createCanvas, setColorMap, renderFrame, getFrameCount, SIZE, FRAMES, STATE_FRAMES };
438
+ /**
439
+ * 커스텀 프레임 데이터 설정 (AI 생성 캐릭터용)
440
+ * 기존 프레임을 백업하고 새 프레임으로 교체
441
+ */
442
+ function setCustomFrames(newFrames) {
443
+ if (!originalFrames) {
444
+ originalFrames = {};
445
+ for (const [key, value] of Object.entries(FRAMES)) {
446
+ originalFrames[key] = JSON.parse(JSON.stringify(value));
447
+ }
448
+ }
449
+ for (const [key, value] of Object.entries(newFrames)) {
450
+ if (Array.isArray(value) && value.length > 0) {
451
+ FRAMES[key] = value;
452
+ }
453
+ }
454
+ }
455
+
456
+ /**
457
+ * 캐릭터 데이터 일괄 설정 (색상 + 프레임)
458
+ * @param {object} data - { colorMap?, frames? }
459
+ */
460
+ function setCharacterData(data) {
461
+ if (data.colorMap) {
462
+ setColorMap(data.colorMap);
463
+ }
464
+ if (data.frames) {
465
+ setCustomFrames(data.frames);
466
+ }
467
+ }
468
+
469
+ /**
470
+ * 원래 캐릭터로 리셋
471
+ */
472
+ function resetCharacter() {
473
+ if (originalFrames) {
474
+ for (const key of Object.keys(FRAMES)) {
475
+ delete FRAMES[key];
476
+ }
477
+ Object.assign(FRAMES, originalFrames);
478
+ originalFrames = null;
479
+ }
480
+ // 기본 색상으로 리셋
481
+ setColorMap({
482
+ primary: '#ff4f40',
483
+ secondary: '#ff775f',
484
+ dark: '#8B4513',
485
+ eye: '#ffffff',
486
+ pupil: '#111111',
487
+ claw: '#ff4f40',
488
+ });
489
+ }
490
+
491
+ return {
492
+ createCanvas, setColorMap, renderFrame, getFrameCount,
493
+ setCustomFrames, setCharacterData, resetCharacter,
494
+ SIZE, FRAMES, STATE_FRAMES,
495
+ };
399
496
  })();