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.
@@ -2,12 +2,12 @@
2
2
  * ClawMate 렌더러 초기화
3
3
  *
4
4
  * 아키텍처:
5
- * OpenClaw AI (뇌) ←→ AI Bridge (WebSocket) ←→ AI Controller (렌더러)
5
+ * AI (뇌) ←→ AI Bridge (WebSocket) ←→ AI Controller (렌더러)
6
6
  * ↓
7
7
  * StateMachine / PetEngine / Speech
8
8
  *
9
- * OpenClaw 연결 시: AI가 모든 행동/말/감정 결정
10
- * OpenClaw 미연결 시: 자율 모드 (FSM 기반) 로 혼자 놀기
9
+ * AI 연결 시: AI가 모든 행동/말/감정 결정
10
+ * AI 미연결 시: 자율 모드 (FSM 기반) 로 혼자 놀기
11
11
  */
12
12
  (async function initClawMate() {
13
13
  const petContainer = document.getElementById('pet-container');
@@ -45,7 +45,10 @@
45
45
  Interactions.spawnStarEffect();
46
46
  }
47
47
 
48
- // 상태 변화를 OpenClaw에 리포트
48
+ // 모션 히스토리 기록
49
+ Memory.recordMotion(newState);
50
+
51
+ // 상태 변화를 AI에 리포트
49
52
  if (window.clawmate.reportToAI) {
50
53
  window.clawmate.reportToAI('state_change', {
51
54
  from: prevState, to: newState,
@@ -62,7 +65,7 @@
62
65
  // 메모리 초기화 (진화 상태 포함)
63
66
  await Memory.init();
64
67
 
65
- // AI 컨트롤러 초기화 (OpenClaw 연결 관리)
68
+ // AI 컨트롤러 초기화 (AI 연결 관리)
66
69
  AIController.init();
67
70
 
68
71
  // 상호작용 초기화
@@ -92,7 +95,7 @@
92
95
  // AI 연결 상태 표시
93
96
  const connected = await window.clawmate.isAIConnected();
94
97
  if (connected) {
95
- Speech.show('OpenClaw과 연결됨. 지시를 기다리는 중...');
98
+ Speech.show('AI와 연결됨. 지시를 기다리는 중...');
96
99
  } else {
97
100
  Speech.show('안녕! 나 혼자서도 잘 놀 수 있어!');
98
101
  }
@@ -82,7 +82,7 @@ const BrowserWatcher = (() => {
82
82
  /**
83
83
  * AI에 브라우징 컨텍스트 전송
84
84
  * 제목 + 커서 위치 + 화면 캡처를 한번에 전송
85
- * AI(OpenClaw 플러그인)가 분석하고 코멘트 생성
85
+ * AI가 분석하고 코멘트 생성
86
86
  */
87
87
  async function reportBrowsingToAI(title, category, titleChanged) {
88
88
  if (!window.clawmate.reportToAI) return;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 마우스/클릭/드래그 상호작용 시스템
3
3
  *
4
- * AI 연결 시: 이벤트를 OpenClaw에 전달 → AI가 반응 결정
4
+ * AI 연결 시: 이벤트를 AI에 전달 → AI가 반응 결정
5
5
  * AI 미연결 시: 자율 반응 (랜덤 FSM)
6
6
  */
7
7
  const Interactions = (() => {
@@ -53,6 +53,8 @@ const Interactions = (() => {
53
53
  clickTimer = setTimeout(() => {
54
54
  if (clickCount >= 3) {
55
55
  onTripleClick();
56
+ } else if (clickCount === 2) {
57
+ onDoubleClick();
56
58
  } else if (clickCount === 1) {
57
59
  onSingleClick();
58
60
  }
@@ -100,18 +102,24 @@ const Interactions = (() => {
100
102
  PetEngine.start();
101
103
  window.clawmate.setClickThrough(true);
102
104
 
103
- // AI에 드래그 이벤트 리포트
105
+ // AI에 드래그 이벤트 리포트 + 반응 기록
104
106
  if (dragStartPos) {
107
+ const draggedAction = StateMachine.getState();
108
+ Memory.recordReaction(draggedAction, 'drag');
105
109
  AIController.reportDrag(dragStartPos, endPos);
106
110
  }
107
111
  }
108
112
 
109
113
  function onSingleClick() {
110
114
  const pos = PetEngine.getPosition();
115
+ const currentAction = StateMachine.getState();
111
116
 
112
117
  // AI에 클릭 이벤트 리포트
113
118
  AIController.reportClick(pos);
114
119
 
120
+ // 유저 반응 기록 — 현재 행동 중 클릭 = 긍정 반응
121
+ Memory.recordReaction(currentAction, 'click');
122
+
115
123
  // AI 연결 시: AI가 반응 결정 (아무것도 안 함, AI 응답 대기)
116
124
  // AI 미연결 시: 자율 반응
117
125
  if (AIController.isAutonomous()) {
@@ -123,7 +131,38 @@ const Interactions = (() => {
123
131
  spawnHeartEffect();
124
132
  }
125
133
 
134
+ function onDoubleClick() {
135
+ const pos = PetEngine.getPosition();
136
+ const currentAction = StateMachine.getState();
137
+
138
+ // 유저 반응 기록
139
+ Memory.recordReaction(currentAction, 'double_click');
140
+
141
+ // AI에 더블클릭 리포트
142
+ if (window.clawmate.reportToAI) {
143
+ window.clawmate.reportToAI('double_click', { position: pos });
144
+ }
145
+
146
+ // 자율 모드: 더블클릭 = 특별 반응 (점프 + 기분좋음)
147
+ if (AIController.isAutonomous()) {
148
+ StateMachine.forceState('excited');
149
+ PetEngine.jumpTo(
150
+ pos.x + (Math.random() - 0.5) * 200,
151
+ Math.max(100, pos.y - 150)
152
+ );
153
+ Speech.show('우와! 더블클릭이다!');
154
+ }
155
+
156
+ Memory.recordClick();
157
+ Memory.recordClick(); // 더블클릭 = 2회 클릭
158
+ spawnHeartEffect();
159
+ spawnStarEffect();
160
+ }
161
+
126
162
  function onTripleClick() {
163
+ const currentAction = StateMachine.getState();
164
+ Memory.recordReaction(currentAction, 'triple_click');
165
+
127
166
  if (typeof ModeManager !== 'undefined') {
128
167
  ModeManager.toggle();
129
168
  }
@@ -139,6 +178,10 @@ const Interactions = (() => {
139
178
  const dist = Math.hypot(e.clientX - (pos.x + 32), e.clientY - (pos.y + 32));
140
179
 
141
180
  if (dist < 100) {
181
+ // 유저 반응 기록 — 커서 접근 = 관심 표현
182
+ const curAction = StateMachine.getState();
183
+ Memory.recordReaction(curAction, 'cursor_near');
184
+
142
185
  // AI에 커서 접근 리포트
143
186
  AIController.reportCursorNear(dist);
144
187
 
@@ -14,8 +14,22 @@ const Memory = (() => {
14
14
  milestones: [],
15
15
  evolutionStage: 0,
16
16
  interactionStreak: 0, // 연속 방문 일수
17
+
18
+ // --- 모션 히스토리 ---
19
+ motionHistory: [], // 최근 100개 상태 전환 기록 [{state, timestamp, duration}]
20
+ motionStats: {}, // 상태별 누적 시간 {idle: 12345, walking: 6789, ...}
21
+
22
+ // --- 유저 반응 저장 ---
23
+ reactionLog: [], // 최근 50개 유저 반응 [{action, reaction, timestamp}]
24
+ favoriteActions: {}, // 행동별 긍정 반응 횟수 {excited: 5, walking: 2, ...}
25
+ dislikedActions: {}, // 행동별 부정 반응 횟수 (무시/이탈)
17
26
  };
18
27
 
28
+ let lastMotionState = null;
29
+ let lastMotionTime = 0;
30
+ const MAX_MOTION_HISTORY = 100;
31
+ const MAX_REACTION_LOG = 50;
32
+
19
33
  let evolutionStages = null;
20
34
 
21
35
  async function init() {
@@ -304,6 +318,95 @@ const Memory = (() => {
304
318
  container.appendChild(acc);
305
319
  }
306
320
 
321
+ // --- 모션 히스토리 기록 ---
322
+
323
+ /**
324
+ * 상태 전환 기록
325
+ * StateMachine에서 상태 변경 시 호출
326
+ */
327
+ function recordMotion(newState) {
328
+ const now = Date.now();
329
+
330
+ // 이전 상태의 지속 시간 계산 → 통계 누적
331
+ if (lastMotionState && lastMotionTime > 0) {
332
+ const duration = now - lastMotionTime;
333
+ if (!data.motionStats[lastMotionState]) data.motionStats[lastMotionState] = 0;
334
+ data.motionStats[lastMotionState] += duration;
335
+ }
336
+
337
+ // 히스토리에 추가
338
+ data.motionHistory.push({
339
+ state: newState,
340
+ timestamp: now,
341
+ from: lastMotionState || 'init',
342
+ });
343
+
344
+ // 최대 크기 초과 시 오래된 것 제거
345
+ if (data.motionHistory.length > MAX_MOTION_HISTORY) {
346
+ data.motionHistory = data.motionHistory.slice(-MAX_MOTION_HISTORY);
347
+ }
348
+
349
+ lastMotionState = newState;
350
+ lastMotionTime = now;
351
+
352
+ // 10회 전환마다 자동 저장
353
+ if (data.motionHistory.length % 10 === 0) save();
354
+ }
355
+
356
+ /**
357
+ * 유저 반응 기록
358
+ * 특정 행동 중 사용자가 클릭/드래그 등의 반응을 보인 경우
359
+ *
360
+ * @param {string} action - 펫이 하고 있던 행동
361
+ * @param {string} reaction - 'click' | 'drag' | 'cursor_near' | 'triple_click' | 'double_click'
362
+ */
363
+ function recordReaction(action, reaction) {
364
+ const now = Date.now();
365
+
366
+ // 반응 로그 추가
367
+ data.reactionLog.push({ action, reaction, timestamp: now });
368
+ if (data.reactionLog.length > MAX_REACTION_LOG) {
369
+ data.reactionLog = data.reactionLog.slice(-MAX_REACTION_LOG);
370
+ }
371
+
372
+ // 클릭/더블클릭은 긍정 반응으로 분류
373
+ if (reaction === 'click' || reaction === 'double_click' || reaction === 'cursor_near') {
374
+ if (!data.favoriteActions[action]) data.favoriteActions[action] = 0;
375
+ data.favoriteActions[action]++;
376
+ }
377
+ // 드래그(잡아서 옮김)는 약간 부정 반응
378
+ if (reaction === 'drag') {
379
+ if (!data.dislikedActions[action]) data.dislikedActions[action] = 0;
380
+ data.dislikedActions[action]++;
381
+ }
382
+
383
+ save();
384
+ }
385
+
386
+ /**
387
+ * 사용자 선호 행동 Top N 반환
388
+ * AI가 행동 결정 시 참고
389
+ */
390
+ function getFavoriteActions(topN = 5) {
391
+ const entries = Object.entries(data.favoriteActions || {});
392
+ entries.sort((a, b) => b[1] - a[1]);
393
+ return entries.slice(0, topN).map(([action, count]) => ({ action, count }));
394
+ }
395
+
396
+ /**
397
+ * 최근 모션 히스토리 반환
398
+ */
399
+ function getMotionHistory(limit = 20) {
400
+ return (data.motionHistory || []).slice(-limit);
401
+ }
402
+
403
+ /**
404
+ * 상태별 누적 시간 반환
405
+ */
406
+ function getMotionStats() {
407
+ return { ...(data.motionStats || {}) };
408
+ }
409
+
307
410
  async function save() {
308
411
  try {
309
412
  await window.clawmate.saveMemory(data);
@@ -318,5 +421,9 @@ const Memory = (() => {
318
421
  return data.evolutionStage;
319
422
  }
320
423
 
321
- return { init, recordClick, getData, getEvolutionStage, save };
424
+ return {
425
+ init, recordClick, getData, getEvolutionStage, save,
426
+ recordMotion, recordReaction, getFavoriteActions,
427
+ getMotionHistory, getMotionStats,
428
+ };
322
429
  })();
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 펫 동작 품질 실시간 계측기 (Self-Observation System)
3
3
  *
4
- * OpenClaw이 자신의 동작 품질을 관찰하고 계량할 수 있도록
4
+ * ClawMate가 자신의 동작 품질을 관찰하고 계량할 수 있도록
5
5
  * 렌더러 측에서 다양한 메트릭을 수집하여 main process로 전송한다.
6
6
  *
7
7
  * 수집 메트릭:
@@ -290,7 +290,7 @@ const Metrics = (() => {
290
290
  }
291
291
  }
292
292
 
293
- // OpenClaw에 상태 변화 리포트
293
+ // AI에 상태 변화 리포트
294
294
  if (window.clawmate && window.clawmate.reportToAI) {
295
295
  window.clawmate.reportToAI('state_change', {
296
296
  from: prevState, to: newState,
@@ -22,14 +22,32 @@ const ModeManager = (() => {
22
22
  const p = personalities[mode];
23
23
  if (!p) return;
24
24
 
25
+ // Incarnation 모드: 활성 인격체가 있으면 반영
26
+ const persona = (mode === 'incarnation' && window._persona)
27
+ ? window._persona.getActivePersona()
28
+ : null;
29
+
25
30
  // 캐릭터 색상 업데이트
26
- const colors = mode === 'pet'
27
- ? { primary: '#ff4f40', secondary: '#ff775f', dark: '#8B4513', eye: '#ffffff', pupil: '#111111', claw: '#ff4f40' }
28
- : { primary: '#ff4f40', secondary: '#ff775f', dark: '#8B4513', eye: '#00BFA5', pupil: '#004D40', claw: '#ff4f40' };
31
+ let colors;
32
+ if (mode === 'pet') {
33
+ colors = { primary: '#ff4f40', secondary: '#ff775f', dark: '#8B4513', eye: '#ffffff', pupil: '#111111', claw: '#ff4f40' };
34
+ } else if (persona?.color) {
35
+ // 인격체 커스텀 색상
36
+ colors = {
37
+ primary: persona.color.primary || '#ff4f40',
38
+ secondary: persona.color.secondary || '#ff775f',
39
+ dark: persona.color.dark || '#8B4513',
40
+ eye: persona.color.eye || '#00BFA5',
41
+ pupil: persona.color.pupil || '#004D40',
42
+ claw: persona.color.claw || '#ff4f40',
43
+ };
44
+ } else {
45
+ colors = { primary: '#ff4f40', secondary: '#ff775f', dark: '#8B4513', eye: '#00BFA5', pupil: '#004D40', claw: '#ff4f40' };
46
+ }
29
47
  Character.setColorMap(colors);
30
48
 
31
- // 속도 조정
32
- PetEngine.setSpeedMultiplier(p.speedMultiplier);
49
+ // 속도 조정 (인격체 우선)
50
+ PetEngine.setSpeedMultiplier(persona?.speedMultiplier ?? p.speedMultiplier);
33
51
 
34
52
  // CSS 클래스
35
53
  pet.classList.remove('mode-pet', 'mode-incarnation');
@@ -38,8 +56,34 @@ const ModeManager = (() => {
38
56
  // 말풍선 스타일
39
57
  Speech.setMode(mode);
40
58
 
41
- // 성격 적용
42
- StateMachine.setPersonality(p);
59
+ // 성격 적용 (인격체가 있으면 병합)
60
+ if (persona) {
61
+ StateMachine.setPersonality({
62
+ ...p,
63
+ playfulness: persona.playfulness ?? p.playfulness,
64
+ shyness: persona.shyness ?? p.shyness,
65
+ boldness: persona.boldness ?? p.boldness,
66
+ });
67
+ } else {
68
+ StateMachine.setPersonality(p);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * 인격체 변경 (Incarnation 모드에서 봇 전환 시)
74
+ */
75
+ function setPersona(personaData) {
76
+ if (window._persona) {
77
+ window._persona.setActivePersona(personaData);
78
+ // 현재 Incarnation 모드면 즉시 반영
79
+ if (currentMode === 'incarnation') {
80
+ applyMode('incarnation');
81
+ }
82
+ }
83
+ }
84
+
85
+ function getPersona() {
86
+ return window._persona ? window._persona.getActivePersona() : null;
43
87
  }
44
88
 
45
89
  async function toggle() {
@@ -50,7 +94,7 @@ const ModeManager = (() => {
50
94
  spawnTransitionEffect(newMode);
51
95
  Speech.show(newMode === 'pet'
52
96
  ? 'Clawby 모드로 변신!'
53
- : 'OpenClaw... 각성했다.');
97
+ : 'Claw... 각성했다.');
54
98
  }
55
99
 
56
100
  function spawnTransitionEffect(mode) {
@@ -78,5 +122,5 @@ const ModeManager = (() => {
78
122
  return currentMode;
79
123
  }
80
124
 
81
- return { init, toggle, getMode, applyMode };
125
+ return { init, toggle, getMode, applyMode, setPersona, getPersona };
82
126
  })();
@@ -25,9 +25,9 @@
25
25
 
26
26
  <div class="mode-card" id="mode-incarnation" onclick="selectMode('incarnation')">
27
27
  <div class="mode-icon">\u{1F980}</div>
28
- <h2>OpenClaw</h2>
28
+ <h2>Claw</h2>
29
29
  <p>육체를 얻은 존재</p>
30
- <span class="mode-desc">침착하고 대담한 OpenClaw이 바탕화면에 강림합니다.</span>
30
+ <span class="mode-desc">침착하고 대담한 Claw가 바탕화면에 강림합니다.</span>
31
31
  </div>
32
32
  </div>
33
33
 
@@ -45,7 +45,7 @@
45
45
  document.getElementById('mode-' + mode).classList.add('selected');
46
46
  const btn = document.getElementById('start-btn');
47
47
  btn.disabled = false;
48
- btn.textContent = mode === 'pet' ? 'Clawby 시작!' : 'OpenClaw 각성!';
48
+ btn.textContent = mode === 'pet' ? 'Clawby 시작!' : 'Claw 각성!';
49
49
  }
50
50
 
51
51
  async function startPet() {
@@ -16,7 +16,7 @@ const PERSONALITIES = {
16
16
  sleepResistance: 0.2, // 수면 저항 (낮음=잘 잠)
17
17
  },
18
18
  incarnation: {
19
- name: 'OpenClaw',
19
+ name: 'Claw',
20
20
  title: '육체를 얻은 존재',
21
21
  playfulness: 0.3,
22
22
  shyness: 0.1,
@@ -28,6 +28,40 @@ const PERSONALITIES = {
28
28
  },
29
29
  };
30
30
 
31
+ /**
32
+ * 동적 인격체 (Incarnation 모드에서 봇의 인격을 반영)
33
+ *
34
+ * 사용자가 여러 봇을 쓸 때, 현재 포커싱된 채팅의 봇 인격체가 반영됨.
35
+ * set_persona 명령으로 동적 업데이트 가능.
36
+ */
37
+ let activePersona = null;
38
+
39
+ function setActivePersona(persona) {
40
+ activePersona = {
41
+ name: persona.name || 'Claw',
42
+ title: persona.title || '육체를 얻은 존재',
43
+ personality: persona.personality || '', // "침착하고 논리적인", "활발하고 유머러스한"
44
+ speakingStyle: persona.speakingStyle || '', // "존댓말", "반말", "도도한"
45
+ color: persona.color || null, // { primary, secondary, eye } 커스텀 색상
46
+ playfulness: persona.playfulness ?? 0.3,
47
+ shyness: persona.shyness ?? 0.1,
48
+ boldness: persona.boldness ?? 0.9,
49
+ speedMultiplier: persona.speedMultiplier ?? 1.0,
50
+ idleChatterChance: persona.idleChatterChance ?? 0.08,
51
+ greetings: persona.greetings || [], // 커스텀 인사말 목록
52
+ catchphrases: persona.catchphrases || [], // 특징적 말버릇
53
+ };
54
+ return activePersona;
55
+ }
56
+
57
+ function getActivePersona() {
58
+ return activePersona;
59
+ }
60
+
61
+ function clearActivePersona() {
62
+ activePersona = null;
63
+ }
64
+
31
65
  /**
32
66
  * 진화 단계별 외형 변화 파라미터
33
67
  * 모든 진화는 긍정적/귀여운 방향으로만 진행
@@ -101,6 +135,7 @@ const EVOLUTION_STAGES = {
101
135
  if (typeof window !== 'undefined') {
102
136
  window._personalities = PERSONALITIES;
103
137
  window._evolutionStages = EVOLUTION_STAGES;
138
+ window._persona = { setActivePersona, getActivePersona, clearActivePersona };
104
139
  } else if (typeof module !== 'undefined') {
105
- module.exports = { PERSONALITIES, EVOLUTION_STAGES };
140
+ module.exports = { PERSONALITIES, EVOLUTION_STAGES, setActivePersona, getActivePersona, clearActivePersona };
106
141
  }
@@ -48,7 +48,7 @@ module.exports = {
48
48
  child.unref();
49
49
 
50
50
  const mode = context.params?.mode || 'pet';
51
- const modeName = mode === 'pet' ? 'Clawby' : 'OpenClaw';
51
+ const modeName = mode === 'pet' ? 'Clawby' : 'Claw';
52
52
 
53
53
  return {
54
54
  success: true,
@@ -34,7 +34,7 @@
34
34
  "type": "string",
35
35
  "enum": ["pet", "incarnation"],
36
36
  "default": "pet",
37
- "description": "시작 모드 (pet: Clawby, incarnation: OpenClaw)"
37
+ "description": "시작 모드 (pet: Clawby, incarnation: Claw)"
38
38
  }
39
39
  }
40
40
  }