@aiyiran/myclaw 1.0.104 → 1.0.112
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/assets/myclaw-inject.js +2 -96
- package/assets/myclaw-tts.js +270 -0
- package/create_agent.js +4 -3
- package/fix.js +277 -0
- package/index.js +190 -4
- package/package.json +1 -1
- package/patch-manifest.json +1 -1
- package/patch.js +20 -1
- package/wizards/scripts/create_agent_steps.js +2 -0
- package/assets/openclaw.ico +0 -0
- package/lobster-icon-generator.html +0 -309
package/assets/myclaw-inject.js
CHANGED
|
@@ -9,11 +9,10 @@
|
|
|
9
9
|
* - 光标处插入文字
|
|
10
10
|
* - 持续录入,手动停止
|
|
11
11
|
* - 讯飞 60 秒断开后自动重连
|
|
12
|
-
* 3. 拦截 chat-tts-btn 按钮,点击后使用讯飞 TTS 播放消息
|
|
13
12
|
*
|
|
14
13
|
* 依赖:
|
|
15
14
|
* - voice-input.js(讯飞 VoiceInput SDK)
|
|
16
|
-
* -
|
|
15
|
+
* - myclaw-tts.js(TTS 按钮绑定,由 myclaw-tts.js 提供)
|
|
17
16
|
* ============================================================================
|
|
18
17
|
*/
|
|
19
18
|
(function () {
|
|
@@ -23,13 +22,11 @@
|
|
|
23
22
|
|
|
24
23
|
// ═══ 状态 ═══
|
|
25
24
|
var voice = null; // VoiceInput 实例
|
|
26
|
-
var tts = null; // VoiceOutput 实例
|
|
27
25
|
var recording = false; // 用户层面的录音状态(独立于 SDK 的 status)
|
|
28
26
|
var pendingText = ""; // 当前这轮识别的文字(实时更新)
|
|
29
27
|
var committedText = ""; // 已经提交到 textarea 的文字(上一轮累积)
|
|
30
28
|
var cursorOffset = 0; // 录音开始时光标在 textarea 中的位置
|
|
31
29
|
var injected = false;
|
|
32
|
-
var ttsBtnHooked = false; // TTS 按钮是否已绑定
|
|
33
30
|
|
|
34
31
|
// ═══ 1. 右下角版本标签(点击测试麦克风) ═══
|
|
35
32
|
function createVersionBar() {
|
|
@@ -401,92 +398,6 @@
|
|
|
401
398
|
console.log("[myclaw-inject] ✅ 发送按钮拦截器已挂载");
|
|
402
399
|
}
|
|
403
400
|
|
|
404
|
-
// ═══ 7. TTS 按钮绑定(讯飞语音合成) ═══
|
|
405
|
-
|
|
406
|
-
function initTts() {
|
|
407
|
-
if (typeof window.VoiceOutput === "undefined") {
|
|
408
|
-
console.error("[myclaw-tts] VoiceOutput SDK 未加载");
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
if (tts) return;
|
|
412
|
-
|
|
413
|
-
tts = new window.VoiceOutput({
|
|
414
|
-
APPID: 'de6ec59e',
|
|
415
|
-
APISecret: 'ZTA3ZDU3YjAyNDUyZGVkNjQwMGM5MWNi',
|
|
416
|
-
APIKey: '37104a0463a5460d7571869324b667d5',
|
|
417
|
-
vcn: 'x4_yezi', // 小露 - 普通话女声
|
|
418
|
-
onAudio: function (audioBuffer) {
|
|
419
|
-
console.log('[myclaw-tts] audio chunk:', audioBuffer.byteLength, 'bytes');
|
|
420
|
-
},
|
|
421
|
-
onStatusChange: function (oldStatus, newStatus) {
|
|
422
|
-
console.log('[myclaw-tts] status:', oldStatus, '->', newStatus);
|
|
423
|
-
},
|
|
424
|
-
onError: function (err) {
|
|
425
|
-
console.error('[myclaw-tts] error:', err);
|
|
426
|
-
}
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
console.log("[myclaw-tts] ✅ 讯飞 TTS SDK 已初始化");
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* 从 .chat-tts-btn 按钮获取对应的消息文字
|
|
434
|
-
* DOM 结构:
|
|
435
|
-
* .chat-group-messages
|
|
436
|
-
* .chat-bubble
|
|
437
|
-
* .chat-text > p (消息文字)
|
|
438
|
-
* .chat-group-footer
|
|
439
|
-
* .chat-tts-btn
|
|
440
|
-
*/
|
|
441
|
-
function extractMessageText(ttsBtn) {
|
|
442
|
-
// 向上找到 .chat-group-messages
|
|
443
|
-
var group = ttsBtn.closest('.chat-group-messages');
|
|
444
|
-
if (!group) return '';
|
|
445
|
-
|
|
446
|
-
// 在 group 内找 .chat-bubble
|
|
447
|
-
var bubble = group.querySelector('.chat-bubble');
|
|
448
|
-
if (!bubble) return '';
|
|
449
|
-
|
|
450
|
-
// 在气泡内找 .chat-text 的文字
|
|
451
|
-
var chatText = bubble.querySelector('.chat-text');
|
|
452
|
-
if (!chatText) return '';
|
|
453
|
-
|
|
454
|
-
// 获取文字,移除多余空白
|
|
455
|
-
var text = chatText.textContent || '';
|
|
456
|
-
return text.replace(/\s+/g, ' ').trim();
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function hookTtsButton() {
|
|
460
|
-
if (ttsBtnHooked) return;
|
|
461
|
-
ttsBtnHooked = true;
|
|
462
|
-
|
|
463
|
-
// 使用捕获阶段拦截点击
|
|
464
|
-
document.addEventListener('click', function (e) {
|
|
465
|
-
var btn = e.target.closest('.chat-tts-btn');
|
|
466
|
-
if (!btn) return;
|
|
467
|
-
|
|
468
|
-
e.preventDefault();
|
|
469
|
-
e.stopPropagation();
|
|
470
|
-
|
|
471
|
-
var text = extractMessageText(btn);
|
|
472
|
-
if (!text) {
|
|
473
|
-
console.warn('[myclaw-tts] 未找到消息文字');
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
console.log('[myclaw-tts] 播放:', text.substring(0, 50) + (text.length > 50 ? '...' : ''));
|
|
478
|
-
|
|
479
|
-
if (!tts) {
|
|
480
|
-
initTts();
|
|
481
|
-
}
|
|
482
|
-
if (tts) {
|
|
483
|
-
tts.speak(text);
|
|
484
|
-
}
|
|
485
|
-
}, true);
|
|
486
|
-
|
|
487
|
-
console.log("[myclaw-inject] ✅ TTS 按钮拦截器已挂载");
|
|
488
|
-
}
|
|
489
|
-
|
|
490
401
|
function fallbackCopy(text) {
|
|
491
402
|
var ta = document.createElement("textarea");
|
|
492
403
|
ta.value = text;
|
|
@@ -506,9 +417,6 @@
|
|
|
506
417
|
// 初始化 VoiceInput SDK
|
|
507
418
|
initVoice();
|
|
508
419
|
|
|
509
|
-
// 初始化 TTS SDK
|
|
510
|
-
initTts();
|
|
511
|
-
|
|
512
420
|
// 持续监听 DOM 变化,确保按钮始终在
|
|
513
421
|
new MutationObserver(function () {
|
|
514
422
|
if (!document.querySelector("#myclaw-voice-btn")) {
|
|
@@ -524,9 +432,7 @@
|
|
|
524
432
|
|
|
525
433
|
// 挂载发送拦截
|
|
526
434
|
hookSendButton();
|
|
527
|
-
|
|
528
|
-
// 挂载 TTS 按钮拦截
|
|
529
|
-
hookTtsButton();
|
|
435
|
+
// TTS 按钮绑定由 myclaw-tts.js 独立处理
|
|
530
436
|
}
|
|
531
437
|
|
|
532
438
|
if (document.readyState === "loading") {
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* MyClaw TTS — 讯飞语音合成
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* 功能:拦截 chat-tts-btn 按钮,点击后使用讯飞 TTS 播放消息
|
|
7
|
+
*
|
|
8
|
+
* 状态:
|
|
9
|
+
* idle → 默认状态,显示小嘴巴 emoji
|
|
10
|
+
* generating → 合成中,显示加载动画
|
|
11
|
+
* playing → 播放中,显示喇叭 emoji,点击可停止
|
|
12
|
+
*
|
|
13
|
+
* 依赖:voice-output.js(讯飞 VoiceOutput SDK,需先于本脚本加载)
|
|
14
|
+
* ============================================================================
|
|
15
|
+
*/
|
|
16
|
+
(function () {
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
// ═══ 状态 ═══
|
|
20
|
+
var tts = null; // VoiceOutput 实例
|
|
21
|
+
var ttsBtnHooked = false;
|
|
22
|
+
var currentBtn = null; // 当前正在播放的按钮
|
|
23
|
+
var playerState = 'idle'; // idle | generating | playing
|
|
24
|
+
|
|
25
|
+
// ═══ 讯飞配置 ═══
|
|
26
|
+
var IFLYTEK_CONFIG = {
|
|
27
|
+
APPID: 'de6ec59e',
|
|
28
|
+
APISecret: 'ZTA3ZDU3YjAyNDUyZGVkNjQwMGM5MWNi',
|
|
29
|
+
APIKey: '37104a0463a5460d7571869324b667d5',
|
|
30
|
+
vcn: 'x4_yezi', // 默认音色:小露(普通话女声)
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ═══ 公开配置接口 ═══
|
|
34
|
+
window.MyClawTts = {
|
|
35
|
+
setVcn: function (vcn) {
|
|
36
|
+
IFLYTEK_CONFIG.vcn = vcn;
|
|
37
|
+
if (tts) tts.vcn = vcn;
|
|
38
|
+
},
|
|
39
|
+
getVcn: function () {
|
|
40
|
+
return IFLYTEK_CONFIG.vcn;
|
|
41
|
+
},
|
|
42
|
+
speak: function (text) {
|
|
43
|
+
if (!tts) initTts();
|
|
44
|
+
if (tts) tts.speak(text);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ═══ 按钮图标 ═══
|
|
49
|
+
var ICONS = {
|
|
50
|
+
idle: '👁', // 👁 小嘴巴
|
|
51
|
+
generating: '🔄', // 🔄 加载中
|
|
52
|
+
playing: '🔊', // 🔊 喇叭
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ═══ 状态更新 ═══
|
|
56
|
+
function setPlayerState(btn, state) {
|
|
57
|
+
if (!btn) return;
|
|
58
|
+
playerState = state;
|
|
59
|
+
|
|
60
|
+
// 恢复原生 SVG
|
|
61
|
+
var svg = btn.querySelector('svg');
|
|
62
|
+
if (svg) {
|
|
63
|
+
svg.style.display = state === 'idle' ? '' : 'none';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 移除所有状态 class
|
|
67
|
+
btn.classList.remove('myclaw-tts--generating', 'myclaw-tts--playing');
|
|
68
|
+
|
|
69
|
+
if (state === 'generating') {
|
|
70
|
+
btn.classList.add('myclaw-tts--generating');
|
|
71
|
+
btn.title = '生成中...';
|
|
72
|
+
} else if (state === 'playing') {
|
|
73
|
+
btn.classList.add('myclaw-tts--playing');
|
|
74
|
+
btn.title = '点击停止';
|
|
75
|
+
} else {
|
|
76
|
+
btn.title = '朗读';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function updateBtnIcon(btn, state) {
|
|
81
|
+
if (!btn) return;
|
|
82
|
+
|
|
83
|
+
// 移除旧的 emoji 容器
|
|
84
|
+
var oldEmoji = btn.querySelector('.myclaw-tts-icon');
|
|
85
|
+
if (oldEmoji) oldEmoji.remove();
|
|
86
|
+
|
|
87
|
+
// 创建 emoji 容器
|
|
88
|
+
var emoji = document.createElement('span');
|
|
89
|
+
emoji.className = 'myclaw-tts-icon';
|
|
90
|
+
emoji.innerHTML = ICONS[state];
|
|
91
|
+
emoji.style.cssText = 'display:inline-block;width:18px;height:18px;line-height:18px;text-align:center;';
|
|
92
|
+
|
|
93
|
+
// 插入到按钮最前面
|
|
94
|
+
btn.insertBefore(emoji, btn.firstChild);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ═══ 初始化 TTS ═══
|
|
98
|
+
function initTts() {
|
|
99
|
+
if (typeof window.VoiceOutput === 'undefined') {
|
|
100
|
+
console.error('[myclaw-tts] VoiceOutput SDK 未加载');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (tts) return;
|
|
104
|
+
|
|
105
|
+
tts = new window.VoiceOutput({
|
|
106
|
+
APPID: IFLYTEK_CONFIG.APPID,
|
|
107
|
+
APISecret: IFLYTEK_CONFIG.APISecret,
|
|
108
|
+
APIKey: IFLYTEK_CONFIG.APIKey,
|
|
109
|
+
vcn: IFLYTEK_CONFIG.vcn,
|
|
110
|
+
onAudio: function (audioBuffer) {
|
|
111
|
+
// console.log('[myclaw-tts] audio chunk:', audioBuffer.byteLength, 'bytes');
|
|
112
|
+
},
|
|
113
|
+
onStatusChange: function (oldStatus, newStatus) {
|
|
114
|
+
console.log('[myclaw-tts] status:', oldStatus, '->', newStatus);
|
|
115
|
+
|
|
116
|
+
if (!currentBtn) return;
|
|
117
|
+
|
|
118
|
+
if (newStatus === 'connecting' || newStatus === 'synthesizing') {
|
|
119
|
+
updateBtnIcon(currentBtn, 'generating');
|
|
120
|
+
setPlayerState(currentBtn, 'generating');
|
|
121
|
+
} else if (newStatus === 'idle') {
|
|
122
|
+
if (playerState === 'generating') {
|
|
123
|
+
// 合成完成但未开始播放(可能是空文字或其他错误)
|
|
124
|
+
setPlayerState(currentBtn, 'idle');
|
|
125
|
+
}
|
|
126
|
+
// playing 状态会在 audio.onended 时处理
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
onError: function (err) {
|
|
130
|
+
console.error('[myclaw-tts] error:', err);
|
|
131
|
+
if (currentBtn) {
|
|
132
|
+
setPlayerState(currentBtn, 'idle');
|
|
133
|
+
updateBtnIcon(currentBtn, 'idle');
|
|
134
|
+
}
|
|
135
|
+
currentBtn = null;
|
|
136
|
+
playerState = 'idle';
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log('[myclaw-tts] ✅ 讯飞 TTS SDK 已初始化');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ═══ 提取消息文字 ═══
|
|
144
|
+
function extractMessageText(ttsBtn) {
|
|
145
|
+
var group = ttsBtn.closest('.chat-group-messages');
|
|
146
|
+
if (!group) return '';
|
|
147
|
+
|
|
148
|
+
var bubble = group.querySelector('.chat-bubble');
|
|
149
|
+
if (!bubble) return '';
|
|
150
|
+
|
|
151
|
+
var chatText = bubble.querySelector('.chat-text');
|
|
152
|
+
if (!chatText) return '';
|
|
153
|
+
|
|
154
|
+
var text = chatText.textContent || '';
|
|
155
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ═══ 注入样式 ═══
|
|
159
|
+
function injectStyles() {
|
|
160
|
+
if (document.querySelector('#myclaw-tts-styles')) return;
|
|
161
|
+
var style = document.createElement('style');
|
|
162
|
+
style.id = 'myclaw-tts-styles';
|
|
163
|
+
style.textContent = [
|
|
164
|
+
/* 生成中:旋转动画 */
|
|
165
|
+
'.myclaw-tts--generating {',
|
|
166
|
+
' animation: myclaw-tts-spin 1s linear infinite;',
|
|
167
|
+
' opacity: 0.7;',
|
|
168
|
+
'}',
|
|
169
|
+
'@keyframes myclaw-tts-spin {',
|
|
170
|
+
' from { transform: rotate(0deg); }',
|
|
171
|
+
' to { transform: rotate(360deg); }',
|
|
172
|
+
'}',
|
|
173
|
+
/* 播放中:脉冲动画 */
|
|
174
|
+
'.myclaw-tts--playing {',
|
|
175
|
+
' animation: myclaw-tts-pulse 0.8s ease-in-out infinite;',
|
|
176
|
+
'}',
|
|
177
|
+
'@keyframes myclaw-tts-pulse {',
|
|
178
|
+
' 0%, 100% { opacity: 1; }',
|
|
179
|
+
' 50% { opacity: 0.5; }',
|
|
180
|
+
'}',
|
|
181
|
+
].join('\n');
|
|
182
|
+
document.head.appendChild(style);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ═══ 挂载按钮拦截 ═══
|
|
186
|
+
function hookTtsButton() {
|
|
187
|
+
if (ttsBtnHooked) return;
|
|
188
|
+
ttsBtnHooked = true;
|
|
189
|
+
|
|
190
|
+
document.addEventListener('click', function (e) {
|
|
191
|
+
var btn = e.target.closest('.chat-tts-btn');
|
|
192
|
+
if (!btn) return;
|
|
193
|
+
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
e.stopPropagation();
|
|
196
|
+
|
|
197
|
+
// 如果正在播放,点击停止
|
|
198
|
+
if (playerState === 'playing' && currentBtn === btn) {
|
|
199
|
+
console.log('[myclaw-tts] 停止播放');
|
|
200
|
+
if (tts) tts.stop();
|
|
201
|
+
setPlayerState(btn, 'idle');
|
|
202
|
+
updateBtnIcon(btn, 'idle');
|
|
203
|
+
currentBtn = null;
|
|
204
|
+
playerState = 'idle';
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 如果当前有其他按钮在播放,先停掉
|
|
209
|
+
if (playerState === 'playing' && currentBtn && currentBtn !== btn) {
|
|
210
|
+
if (tts) tts.stop();
|
|
211
|
+
setPlayerState(currentBtn, 'idle');
|
|
212
|
+
updateBtnIcon(currentBtn, 'idle');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
var text = extractMessageText(btn);
|
|
216
|
+
if (!text) {
|
|
217
|
+
console.warn('[myclaw-tts] 未找到消息文字');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
console.log('[myclaw-tts] 播放:', text.substring(0, 50) + (text.length > 50 ? '...' : ''));
|
|
222
|
+
|
|
223
|
+
// 设置当前按钮和状态
|
|
224
|
+
currentBtn = btn;
|
|
225
|
+
setPlayerState(btn, 'generating');
|
|
226
|
+
updateBtnIcon(btn, 'generating');
|
|
227
|
+
|
|
228
|
+
// 拦截 onAudioEnd 来设置 playing 状态
|
|
229
|
+
if (!tts) initTts();
|
|
230
|
+
if (tts) {
|
|
231
|
+
tts.onAudioEnd = function () {
|
|
232
|
+
console.log('[myclaw-tts] 播放结束');
|
|
233
|
+
if (currentBtn) {
|
|
234
|
+
setPlayerState(currentBtn, 'idle');
|
|
235
|
+
updateBtnIcon(currentBtn, 'idle');
|
|
236
|
+
currentBtn = null;
|
|
237
|
+
}
|
|
238
|
+
playerState = 'idle';
|
|
239
|
+
};
|
|
240
|
+
tts.vcn = IFLYTEK_CONFIG.vcn;
|
|
241
|
+
tts.speak(text);
|
|
242
|
+
|
|
243
|
+
// 状态会在 onStatusChange 里变成 playing
|
|
244
|
+
// 手动设置一次,因为 Web Audio API 可能比 status 更快
|
|
245
|
+
setTimeout(function () {
|
|
246
|
+
if (playerState === 'generating') {
|
|
247
|
+
setPlayerState(btn, 'playing');
|
|
248
|
+
updateBtnIcon(btn, 'playing');
|
|
249
|
+
playerState = 'playing';
|
|
250
|
+
}
|
|
251
|
+
}, 100);
|
|
252
|
+
}
|
|
253
|
+
}, true);
|
|
254
|
+
|
|
255
|
+
console.log('[myclaw-tts] ✅ TTS 按钮拦截器已挂载');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ═══ 启动 ═══
|
|
259
|
+
function init() {
|
|
260
|
+
injectStyles();
|
|
261
|
+
initTts();
|
|
262
|
+
hookTtsButton();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (document.readyState === 'loading') {
|
|
266
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
267
|
+
} else {
|
|
268
|
+
init();
|
|
269
|
+
}
|
|
270
|
+
})();
|
package/create_agent.js
CHANGED
|
@@ -154,7 +154,8 @@ function defaultWorkspaceFiles(agentId) {
|
|
|
154
154
|
* @param {string} rawName - 原始名称
|
|
155
155
|
* @returns {object} 创建结果
|
|
156
156
|
*/
|
|
157
|
-
function createAgent(rawName) {
|
|
157
|
+
function createAgent(rawName, opts) {
|
|
158
|
+
opts = opts || {};
|
|
158
159
|
// 标准化 & 验证名称
|
|
159
160
|
const agentId = normalizeAgentId(rawName);
|
|
160
161
|
validateAgentId(agentId);
|
|
@@ -239,8 +240,8 @@ function createAgent(rawName) {
|
|
|
239
240
|
fail('配置文件写入失败: ' + err.message + '\n备份文件: ' + normalizePath(backupPath));
|
|
240
241
|
}
|
|
241
242
|
|
|
242
|
-
//
|
|
243
|
-
const birthMessage = buildBirthMessage();
|
|
243
|
+
// 发送首条消息:优先用自定义内容,否则走默认出生文案
|
|
244
|
+
const birthMessage = opts.firstMessage || buildBirthMessage();
|
|
244
245
|
let firstMessageSent = false;
|
|
245
246
|
let firstMessageError = null;
|
|
246
247
|
|
package/fix.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* MyClaw Fix - 兜底修复命令
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* 使用: myclaw fix
|
|
9
|
+
*
|
|
10
|
+
* 运行后自动执行两件事:
|
|
11
|
+
* 1. 强制下载并安装 WSL MSI(无论是否已装,确保补齐)
|
|
12
|
+
* 2. 检查 Chrome 是否已安装,未装则下载并静默安装
|
|
13
|
+
*
|
|
14
|
+
* 设计原则:
|
|
15
|
+
* - 仅限 Windows 使用
|
|
16
|
+
* - 管理员权限运行(MSI 安装需要)
|
|
17
|
+
* - WSL 强制安装(兜底,避免漏装)
|
|
18
|
+
* - Chrome 按需安装(检测到才跳过)
|
|
19
|
+
* - 全自动:学生不需要做任何操作
|
|
20
|
+
* ============================================================================
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const { execSync } = require('child_process');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// 配置
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
const CDN = {
|
|
32
|
+
wsl: 'https://cdn.yiranlaoshi.com/software/myclaw/wsl.2.7.1.0.x64.msi',
|
|
33
|
+
chrome: 'https://cdn.yiranlaoshi.com/software/myclaw/ChromeStandaloneSetup64.exe',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const isWindows = os.platform() === 'win32';
|
|
37
|
+
|
|
38
|
+
const C = isWindows
|
|
39
|
+
? { r: '', g: '', y: '', b: '', nc: '' }
|
|
40
|
+
: { r: '\x1b[31m', g: '\x1b[32m', y: '\x1b[33m', b: '\x1b[34m', nc: '\x1b[0m' };
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// 工具:启动 PowerShell 管理员窗口执行脚本
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
function launchElevatedPS(script) {
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
|
|
49
|
+
const tmpDir = process.env.LOCALAPPDATA
|
|
50
|
+
? path.join(process.env.LOCALAPPDATA, 'myclaw')
|
|
51
|
+
: path.join(os.tmpdir(), 'myclaw');
|
|
52
|
+
try { fs.mkdirSync(tmpDir, { recursive: true }); } catch {}
|
|
53
|
+
const scriptPath = path.join(tmpDir, 'fix_installer.ps1');
|
|
54
|
+
|
|
55
|
+
// UTF-8 with BOM — PowerShell 需要 BOM 才能正确读取中文
|
|
56
|
+
const BOM = '\uFEFF';
|
|
57
|
+
fs.writeFileSync(scriptPath, BOM + script, 'utf8');
|
|
58
|
+
|
|
59
|
+
const escaped = scriptPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
60
|
+
const cmd = `powershell -Command "Start-Process powershell -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-NoExit','-File','${escaped}') -Verb RunAs"`;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
64
|
+
return true;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error('[' + C.r + '错误' + C.nc + '] 无法获取管理员权限: ' + err.message);
|
|
67
|
+
console.log('建议右键终端选择【以管理员身份运行】后重试。');
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// 检测 Chrome
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
function isChromeInstalled() {
|
|
77
|
+
const locations = [
|
|
78
|
+
path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
79
|
+
path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
80
|
+
path.join(os.homedir(), 'AppData', 'Local', 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const fs = require('fs');
|
|
84
|
+
return locations.some(loc => {
|
|
85
|
+
try { fs.accessSync(loc); return true; } catch { return false; }
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// 主入口
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
function run() {
|
|
94
|
+
if (!isWindows) {
|
|
95
|
+
console.error('[' + C.y + '提示' + C.nc + '] 本命令仅用于 Windows 系统。');
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const BAR = '========================================';
|
|
100
|
+
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log('[' + C.b + 'MyClaw' + C.nc + '] ' + C.b + '兜底修复' + C.nc);
|
|
103
|
+
console.log(BAR);
|
|
104
|
+
console.log('');
|
|
105
|
+
|
|
106
|
+
// Chrome 检测(WSL 不检测,强制安装)
|
|
107
|
+
const chromeOK = isChromeInstalled();
|
|
108
|
+
|
|
109
|
+
console.log('[检测结果]');
|
|
110
|
+
console.log(' WSL: ' + C.y + '[强制安装]' + C.nc);
|
|
111
|
+
console.log(' Chrome: ' + (chromeOK ? C.g + '[已安装] 跳过' + C.nc : C.r + '[未安装] 将安装' + C.nc));
|
|
112
|
+
console.log('');
|
|
113
|
+
|
|
114
|
+
const needChrome = !chromeOK;
|
|
115
|
+
const totalSteps = 2 + (needChrome ? 1 : 0);
|
|
116
|
+
|
|
117
|
+
// Node 端也设置一下(当前终端环境)
|
|
118
|
+
try { execSync('npm config set registry https://registry.npmmirror.com', { stdio: 'pipe' }); } catch {}
|
|
119
|
+
|
|
120
|
+
let psScript = `
|
|
121
|
+
$ErrorActionPreference = 'Continue'
|
|
122
|
+
$Host.UI.RawUI.WindowTitle = 'MyClaw Fix'
|
|
123
|
+
try {
|
|
124
|
+
Write-Host ''
|
|
125
|
+
Write-Host '========================================'
|
|
126
|
+
Write-Host ' MyClaw 兜底修复'
|
|
127
|
+
Write-Host '========================================'
|
|
128
|
+
Write-Host ''
|
|
129
|
+
|
|
130
|
+
$dir = "$env:LOCALAPPDATA\\\\myclaw"
|
|
131
|
+
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
|
132
|
+
|
|
133
|
+
# ================================================
|
|
134
|
+
# [1/${totalSteps}] 配置 npm 中国镜像
|
|
135
|
+
# ================================================
|
|
136
|
+
Write-Host '[1/${totalSteps}] 配置 npm 中国镜像...'
|
|
137
|
+
try {
|
|
138
|
+
npm config set registry https://registry.npmmirror.com 2>$null
|
|
139
|
+
Write-Host ' [OK] registry -> https://registry.npmmirror.com'
|
|
140
|
+
} catch {
|
|
141
|
+
Write-Host ' [跳过] npm 未安装或配置失败'
|
|
142
|
+
}
|
|
143
|
+
Write-Host ''
|
|
144
|
+
|
|
145
|
+
# ================================================
|
|
146
|
+
# [2/${totalSteps}] 强制安装 WSL
|
|
147
|
+
# ================================================
|
|
148
|
+
Write-Host '[2/${totalSteps}] 强制安装 WSL...'
|
|
149
|
+
Write-Host ''
|
|
150
|
+
|
|
151
|
+
Write-Host ' [a] 启用 Windows Subsystem for Linux...'
|
|
152
|
+
try {
|
|
153
|
+
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart 2>$null | Out-Null
|
|
154
|
+
Write-Host ' [OK]'
|
|
155
|
+
} catch {
|
|
156
|
+
Write-Host ' 跳过 (可能已启用)'
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
Write-Host ' [b] 启用虚拟机平台...'
|
|
160
|
+
try {
|
|
161
|
+
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart 2>$null | Out-Null
|
|
162
|
+
Write-Host ' [OK]'
|
|
163
|
+
} catch {
|
|
164
|
+
Write-Host ' 跳过 (可能已启用)'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
$msi = "$dir\\\\wsl_fix.msi"
|
|
168
|
+
Write-Host ' [c] 下载 WSL 安装包...'
|
|
169
|
+
try {
|
|
170
|
+
Start-BitsTransfer -Source '${CDN.wsl}' -Destination $msi -Description '下载 WSL' -DisplayName 'MyClaw Fix'
|
|
171
|
+
Write-Host ' 下载完成'
|
|
172
|
+
} catch {
|
|
173
|
+
Write-Host ' BitsTransfer 失败,尝试 Invoke-WebRequest...'
|
|
174
|
+
try {
|
|
175
|
+
Invoke-WebRequest -Uri '${CDN.wsl}' -OutFile $msi -UseBasicParsing
|
|
176
|
+
Write-Host ' 下载完成'
|
|
177
|
+
} catch {
|
|
178
|
+
Write-Host ' [失败] 下载失败,请检查网络'
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (Test-Path $msi) {
|
|
183
|
+
Write-Host ' [d] 静默安装 WSL...'
|
|
184
|
+
Start-Process msiexec.exe -ArgumentList "/i \`\\"$msi\`\\" /quiet /norestart" -Wait -NoNewWindow
|
|
185
|
+
Write-Host ' [OK]'
|
|
186
|
+
}
|
|
187
|
+
Write-Host ''
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
// === Chrome(按需)===
|
|
191
|
+
if (needChrome) {
|
|
192
|
+
psScript += `
|
|
193
|
+
# ================================================
|
|
194
|
+
# [3/${totalSteps}] 安装 Chrome
|
|
195
|
+
# ================================================
|
|
196
|
+
Write-Host '[3/${totalSteps}] 安装 Chrome...'
|
|
197
|
+
Write-Host ''
|
|
198
|
+
|
|
199
|
+
$chromeExists = $false
|
|
200
|
+
$chromePaths = @(
|
|
201
|
+
"$env:ProgramFiles\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe",
|
|
202
|
+
"\${env:ProgramFiles(x86)}\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe",
|
|
203
|
+
"$env:LOCALAPPDATA\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe"
|
|
204
|
+
)
|
|
205
|
+
foreach ($cp in $chromePaths) {
|
|
206
|
+
if (Test-Path $cp) { $chromeExists = $true; break }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if ($chromeExists) {
|
|
210
|
+
Write-Host ' Chrome 已安装,跳过'
|
|
211
|
+
} else {
|
|
212
|
+
$exe = "$dir\\\\ChromeSetup.exe"
|
|
213
|
+
Write-Host ' [a] 下载 Chrome 安装包...'
|
|
214
|
+
try {
|
|
215
|
+
Start-BitsTransfer -Source '${CDN.chrome}' -Destination $exe -Description '下载 Chrome' -DisplayName 'MyClaw Fix'
|
|
216
|
+
Write-Host ' 下载完成'
|
|
217
|
+
} catch {
|
|
218
|
+
Write-Host ' BitsTransfer 失败,尝试 Invoke-WebRequest...'
|
|
219
|
+
try {
|
|
220
|
+
Invoke-WebRequest -Uri '${CDN.chrome}' -OutFile $exe -UseBasicParsing
|
|
221
|
+
Write-Host ' 下载完成'
|
|
222
|
+
} catch {
|
|
223
|
+
Write-Host ' [失败] 下载失败,请检查网络'
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (Test-Path $exe) {
|
|
228
|
+
Write-Host ' [b] 安装 Chrome (静默)...'
|
|
229
|
+
Start-Process $exe -ArgumentList '/silent /install' -Wait -NoNewWindow
|
|
230
|
+
Write-Host ' [OK]'
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
Write-Host ''
|
|
234
|
+
`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// === 结尾 ===
|
|
238
|
+
psScript += `
|
|
239
|
+
Write-Host '========================================'
|
|
240
|
+
Write-Host ' 修复完成!'
|
|
241
|
+
Write-Host '========================================'
|
|
242
|
+
Write-Host ''
|
|
243
|
+
Write-Host ' [重要] WSL 安装后可能需要 重启电脑 才能生效'
|
|
244
|
+
Write-Host ' 重启后运行: myclaw wsl2'
|
|
245
|
+
Write-Host ''
|
|
246
|
+
|
|
247
|
+
} catch {
|
|
248
|
+
Write-Host ''
|
|
249
|
+
Write-Host '========================================'
|
|
250
|
+
Write-Host " [异常] 发生错误: $_"
|
|
251
|
+
Write-Host ' 请截图此窗口内容后联系老师'
|
|
252
|
+
Write-Host '========================================'
|
|
253
|
+
} finally {
|
|
254
|
+
Write-Host ''
|
|
255
|
+
Write-Host '按任意键关闭此窗口...'
|
|
256
|
+
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
|
|
257
|
+
}
|
|
258
|
+
`;
|
|
259
|
+
|
|
260
|
+
// 显示即将执行的操作
|
|
261
|
+
console.log('[即将执行]');
|
|
262
|
+
console.log(' • 强制下载并安装 WSL (' + CDN.wsl + ')');
|
|
263
|
+
if (needChrome) console.log(' • 下载并安装 Chrome (' + CDN.chrome + ')');
|
|
264
|
+
console.log('');
|
|
265
|
+
console.log('[' + C.y + '注意' + C.nc + '] 请在 UAC 弹窗中点击【是】');
|
|
266
|
+
console.log('');
|
|
267
|
+
|
|
268
|
+
if (launchElevatedPS(psScript)) {
|
|
269
|
+
console.log('[' + C.g + '已启动' + C.nc + '] 请查看新弹出的蓝色窗口');
|
|
270
|
+
console.log('');
|
|
271
|
+
console.log('WSL 安装完成后可能需要 ' + C.y + '重启电脑' + C.nc);
|
|
272
|
+
console.log('重启后运行 ' + C.y + 'myclaw wsl2' + C.nc + ' 继续安装');
|
|
273
|
+
console.log('');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = { run };
|
package/index.js
CHANGED
|
@@ -181,8 +181,175 @@ function runStatus() {
|
|
|
181
181
|
// ============================================================================
|
|
182
182
|
|
|
183
183
|
function runNew() {
|
|
184
|
-
const
|
|
185
|
-
|
|
184
|
+
const rawName = args[1];
|
|
185
|
+
|
|
186
|
+
// 无参数 → 向导模式
|
|
187
|
+
if (!rawName) {
|
|
188
|
+
const { runNewAgent } = require('./wizards/index');
|
|
189
|
+
runNewAgent();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 有参数 → 一键创建模式(带分步日志)
|
|
194
|
+
// myclaw new haha → 默认出生文案
|
|
195
|
+
// myclaw new haha 你好见到你很高兴 → 自定义首条消息
|
|
196
|
+
const firstMessage = args.slice(2).join(' ').trim() || null;
|
|
197
|
+
|
|
198
|
+
const bar = '────────────────────────────────────────';
|
|
199
|
+
const G = colors.green;
|
|
200
|
+
const Y = colors.yellow;
|
|
201
|
+
const R = colors.red;
|
|
202
|
+
const B = colors.blue;
|
|
203
|
+
const NC = colors.nc;
|
|
204
|
+
|
|
205
|
+
console.log('');
|
|
206
|
+
console.log('[' + B + 'MyClaw' + NC + '] ' + B + '创建 Agent: ' + rawName + NC);
|
|
207
|
+
console.log(bar);
|
|
208
|
+
console.log('');
|
|
209
|
+
|
|
210
|
+
const { normalizeAgentId, validateAgentId } = require('./create_agent');
|
|
211
|
+
const fs = require('fs');
|
|
212
|
+
const path = require('path');
|
|
213
|
+
const os = require('os');
|
|
214
|
+
const { execSync } = require('child_process');
|
|
215
|
+
|
|
216
|
+
const homeDir = os.homedir();
|
|
217
|
+
const openclawDir = path.join(homeDir, '.openclaw');
|
|
218
|
+
const configPath = path.join(openclawDir, 'openclaw.json');
|
|
219
|
+
|
|
220
|
+
// ── Step 1: 名称标准化 ──
|
|
221
|
+
console.log(B + '[1/6]' + NC + ' 标准化名称...');
|
|
222
|
+
console.log(' ' + G + '▶ 发起:' + NC + ' normalizeAgentId("' + rawName + '")');
|
|
223
|
+
let agentId;
|
|
224
|
+
try {
|
|
225
|
+
agentId = normalizeAgentId(rawName);
|
|
226
|
+
validateAgentId(agentId);
|
|
227
|
+
console.log(' ' + G + '◀ 返回:' + NC + ' agentId = "' + agentId + '" ✅');
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.log(' ' + R + '◀ 返回:' + NC + ' ❌ ' + err.message);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
console.log('');
|
|
233
|
+
|
|
234
|
+
// ── Step 2: 环境检查 ──
|
|
235
|
+
console.log(B + '[2/6]' + NC + ' 环境检查(查重 + 目录冲突)...');
|
|
236
|
+
console.log(' ' + G + '▶ 发起:' + NC + ' 读取 ' + configPath);
|
|
237
|
+
let configData;
|
|
238
|
+
try {
|
|
239
|
+
configData = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
240
|
+
if (!configData.agents) configData.agents = {};
|
|
241
|
+
if (!Array.isArray(configData.agents.list)) configData.agents.list = [];
|
|
242
|
+
|
|
243
|
+
const exists = configData.agents.list.find(a => a && a.id === agentId);
|
|
244
|
+
if (exists) {
|
|
245
|
+
console.log(' ' + R + '◀ 返回:' + NC + ' ❌ Agent "' + agentId + '" 已存在');
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
const workspaceDir = path.join(openclawDir, 'workspace-' + agentId);
|
|
249
|
+
const agentDir = path.join(openclawDir, 'agents', agentId, 'agent');
|
|
250
|
+
if (fs.existsSync(workspaceDir) || fs.existsSync(agentDir)) {
|
|
251
|
+
console.log(' ' + R + '◀ 返回:' + NC + ' ❌ 目录已存在');
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
console.log(' ' + G + '◀ 返回:' + NC + ' 名称无冲突,目录可用 ✅');
|
|
255
|
+
} catch (err) {
|
|
256
|
+
console.log(' ' + R + '◀ 返回:' + NC + ' ❌ ' + err.message);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
console.log('');
|
|
260
|
+
|
|
261
|
+
// ── Step 3: 创建工作空间 ──
|
|
262
|
+
console.log(B + '[3/6]' + NC + ' 创建工作空间(SOUL.md / AGENTS.md / USER.md ...)...');
|
|
263
|
+
const workspaceDir = path.join(openclawDir, 'workspace-' + agentId);
|
|
264
|
+
const agentDir = path.join(openclawDir, 'agents', agentId, 'agent');
|
|
265
|
+
const sessionsDir = path.join(openclawDir, 'agents', agentId, 'sessions');
|
|
266
|
+
console.log(' ' + G + '▶ 发起:' + NC + ' mkdir + writeFile → ' + workspaceDir);
|
|
267
|
+
try {
|
|
268
|
+
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
269
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
270
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
271
|
+
// 写入默认文件
|
|
272
|
+
const defaultFiles = {
|
|
273
|
+
'AGENTS.md': '# AGENTS.md - ' + agentId + '\n\nThis workspace belongs to the `' + agentId + '` agent.\n\n## Startup\n- Read `SOUL.md`\n- Read `USER.md`\n\n## Rules\n- Be helpful, careful, and concise.\n',
|
|
274
|
+
'SOUL.md': '# SOUL.md\n\nYou are `' + agentId + '`.\n\nBe useful, calm, direct, and trustworthy.\n',
|
|
275
|
+
'USER.md': '# USER.md\n\n- Name: 孙依然\n- What to call them: 依然\n- Timezone: Asia/Shanghai\n',
|
|
276
|
+
'IDENTITY.md': '# IDENTITY.md\n\n- Name: ' + agentId + '\n- Role: OpenClaw agent\n',
|
|
277
|
+
'BOOTSTRAP.md': '# BOOTSTRAP.md\n\nYou are a newly created agent named `' + agentId + '`.\nOn first runs, learn your workspace files and begin helping.\n',
|
|
278
|
+
};
|
|
279
|
+
for (const [fn, content] of Object.entries(defaultFiles)) {
|
|
280
|
+
const fp = path.join(workspaceDir, fn);
|
|
281
|
+
if (!fs.existsSync(fp)) fs.writeFileSync(fp, content, 'utf8');
|
|
282
|
+
}
|
|
283
|
+
console.log(' ' + G + '◀ 返回:' + NC + ' 已创建 ' + Object.keys(defaultFiles).length + ' 个基础文件 ✅');
|
|
284
|
+
} catch (err) {
|
|
285
|
+
console.log(' ' + R + '◀ 返回:' + NC + ' ❌ ' + err.message);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
console.log('');
|
|
289
|
+
|
|
290
|
+
// ── Step 4: 注册到配置 ──
|
|
291
|
+
console.log(B + '[4/6]' + NC + ' 注册到 openclaw.json(agents.list)...');
|
|
292
|
+
const backupPath = configPath + '.bak.agent-birth.' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, 26) + 'Z';
|
|
293
|
+
console.log(' ' + G + '▶ 发起:' + NC + ' 备份 → ' + path.basename(backupPath));
|
|
294
|
+
try {
|
|
295
|
+
fs.copyFileSync(configPath, backupPath);
|
|
296
|
+
configData.agents.list.push({
|
|
297
|
+
id: agentId,
|
|
298
|
+
name: agentId,
|
|
299
|
+
workspace: workspaceDir.replace(/\\/g, '/'),
|
|
300
|
+
agentDir: agentDir.replace(/\\/g, '/'),
|
|
301
|
+
});
|
|
302
|
+
fs.writeFileSync(configPath, JSON.stringify(configData, null, 2) + '\n', 'utf8');
|
|
303
|
+
console.log(' ' + G + '◀ 返回:' + NC + ' 配置已写入,备份已保存 ✅');
|
|
304
|
+
} catch (err) {
|
|
305
|
+
console.log(' ' + R + '◀ 返回:' + NC + ' ❌ ' + err.message);
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
console.log('');
|
|
309
|
+
|
|
310
|
+
// ── Step 5: 发送首条消息 ──
|
|
311
|
+
console.log(B + '[5/6]' + NC + ' 发送首条消息(唤醒 Agent)...');
|
|
312
|
+
const nowUtc = new Date();
|
|
313
|
+
const nowBj = new Date(nowUtc.getTime() + 8 * 60 * 60 * 1000);
|
|
314
|
+
const ts = nowBj.getUTCFullYear() + '年' + (nowBj.getUTCMonth() + 1) + '月' + nowBj.getUTCDate() + '日' + String(nowBj.getUTCHours()).padStart(2, '0') + ':' + String(nowBj.getUTCMinutes()).padStart(2, '0');
|
|
315
|
+
const birthMessage = firstMessage || ('你好,你的生日是北京时间' + ts + ',请记录这个信息。\n欢迎你来到这个世界,我是你的造物主,我们是伙伴,我叫做孙依然,你可以叫我依然,请多多指教,共同成长。');
|
|
316
|
+
|
|
317
|
+
const cmd = 'openclaw agent --agent ' + agentId + ' --message ' + JSON.stringify(birthMessage) + ' --json';
|
|
318
|
+
console.log(' ' + G + '▶ 发起:' + NC + ' $ ' + G + cmd.substring(0, 80) + (cmd.length > 80 ? '...' : '') + NC);
|
|
319
|
+
console.log(' ' + Y + ' ⏳ 等待大模型响应(可能需要 5~15 秒)...' + NC);
|
|
320
|
+
|
|
321
|
+
let firstMessageSent = false;
|
|
322
|
+
try {
|
|
323
|
+
execSync(cmd + ' 2>&1', { encoding: 'utf8', timeout: 45000 });
|
|
324
|
+
firstMessageSent = true;
|
|
325
|
+
console.log(' ' + G + '◀ 返回:' + NC + ' 唤醒成功,Session 已建立 ✅');
|
|
326
|
+
} catch (err) {
|
|
327
|
+
console.log(' ' + Y + '◀ 返回:' + NC + ' ⚠️ 唤醒超时或失败(非致命)');
|
|
328
|
+
console.log(' ' + '手动重试: ' + Y + 'openclaw agent --agent ' + agentId + ' --message "你好"' + NC);
|
|
329
|
+
}
|
|
330
|
+
console.log('');
|
|
331
|
+
|
|
332
|
+
// ── Step 6: 验证 ──
|
|
333
|
+
console.log(B + '[6/6]' + NC + ' 验证创建结果...');
|
|
334
|
+
console.log(' ' + G + '▶ 发起:' + NC + ' 检查 agents.list 是否包含 "' + agentId + '"');
|
|
335
|
+
try {
|
|
336
|
+
const verify = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
337
|
+
const found = verify.agents && verify.agents.list && verify.agents.list.find(a => a.id === agentId);
|
|
338
|
+
if (found) {
|
|
339
|
+
console.log(' ' + G + '◀ 返回:' + NC + ' Agent "' + agentId + '" 已成功登记 ✅');
|
|
340
|
+
} else {
|
|
341
|
+
console.log(' ' + R + '◀ 返回:' + NC + ' ⚠️ 未在配置中找到,请检查');
|
|
342
|
+
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
console.log(' ' + Y + '◀ 返回:' + NC + ' ⚠️ 验证读取失败(非致命)');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
console.log('');
|
|
348
|
+
console.log(bar);
|
|
349
|
+
console.log(G + '🎉 Agent "' + agentId + '" 创建完成!' + NC);
|
|
350
|
+
console.log('');
|
|
351
|
+
console.log('下一步: ' + Y + 'openclaw agent --agent ' + agentId + ' --message "你好"' + NC);
|
|
352
|
+
console.log('');
|
|
186
353
|
}
|
|
187
354
|
|
|
188
355
|
// ============================================================================
|
|
@@ -198,6 +365,15 @@ function runRebind() {
|
|
|
198
365
|
runRebind();
|
|
199
366
|
}
|
|
200
367
|
|
|
368
|
+
// ============================================================================
|
|
369
|
+
// Fix 兜底修复 (独立模块)
|
|
370
|
+
// ============================================================================
|
|
371
|
+
|
|
372
|
+
function runFix() {
|
|
373
|
+
const fix = require('./fix');
|
|
374
|
+
fix.run();
|
|
375
|
+
}
|
|
376
|
+
|
|
201
377
|
// ============================================================================
|
|
202
378
|
// WSL2 安装 (独立模块)
|
|
203
379
|
// ============================================================================
|
|
@@ -288,8 +464,15 @@ timeout /t 2 >nul
|
|
|
288
464
|
fs.writeFileSync(batPath, batContent.replace(/\n/g, '\r\n'), 'utf8');
|
|
289
465
|
|
|
290
466
|
// 用 PowerShell 创建带图标的桌面快捷方式 + 刷新桌面
|
|
291
|
-
|
|
467
|
+
// 用 PowerShell 下载图标并创建带图标的桌面快捷方式 + 刷新桌面
|
|
468
|
+
const iconPath = path.join(myClawDir, 'openclaw.ico');
|
|
469
|
+
const iconUrl = 'https://cdn.yiranlaoshi.com/software/myclaw/openclaw.ico';
|
|
470
|
+
|
|
292
471
|
const psScript = `
|
|
472
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
473
|
+
if (-not (Test-Path '${iconPath.replace(/\\/g, '\\\\')}')) {
|
|
474
|
+
Invoke-WebRequest -Uri '${iconUrl}' -OutFile '${iconPath.replace(/\\/g, '\\\\')}' -UseBasicParsing
|
|
475
|
+
}
|
|
293
476
|
$ws = New-Object -ComObject WScript.Shell
|
|
294
477
|
$sc = $ws.CreateShortcut('${lnkPath.replace(/\\/g, '\\\\')}')
|
|
295
478
|
$sc.TargetPath = '${batPath.replace(/\\/g, '\\\\')}'
|
|
@@ -776,7 +959,7 @@ function showHelp() {
|
|
|
776
959
|
console.log('');
|
|
777
960
|
console.log('向导 (交互式):');
|
|
778
961
|
console.log(' prepare 智能初始化(环境检测 + 安装 + Patch)');
|
|
779
|
-
console.log(' new 创建新 Agent
|
|
962
|
+
console.log(' new 创建新 Agent(无参数=向导,有参数=一键)');
|
|
780
963
|
console.log(' weixin 微信接入向导(步骤引导 + 教学说明)');
|
|
781
964
|
console.log(' rebind 微信换绑向导(清空、扫码、换Agent)');
|
|
782
965
|
console.log('');
|
|
@@ -787,6 +970,7 @@ function showHelp() {
|
|
|
787
970
|
console.log(' update 自动升级 MyClaw 到最新版本');
|
|
788
971
|
console.log(' up 升级 + 刷新桌面快捷方式 (= update + bat)');
|
|
789
972
|
console.log(' open 打开浏览器控制台(自动带 token)');
|
|
973
|
+
console.log(' fix 兜底修复(自动补装 WSL + Chrome,仅限 Windows)');
|
|
790
974
|
console.log(' wsl2 WSL2 一键安装/修复 (仅限 Windows)');
|
|
791
975
|
console.log(' bat 在桌面生成一键启动脚本 (仅限 Windows)');
|
|
792
976
|
console.log(' list 查看注入资源管理列表(智能体/技能/配置)');
|
|
@@ -844,6 +1028,8 @@ if (!command || command === 'help' || command === '--help' || command === '-h')
|
|
|
844
1028
|
runStart();
|
|
845
1029
|
} else if (command === 'open') {
|
|
846
1030
|
runOpen();
|
|
1031
|
+
} else if (command === 'fix') {
|
|
1032
|
+
runFix();
|
|
847
1033
|
} else if (command === 'wsl2') {
|
|
848
1034
|
runWsl2();
|
|
849
1035
|
} else if (command === 'launch') {
|
package/package.json
CHANGED
package/patch-manifest.json
CHANGED
package/patch.js
CHANGED
|
@@ -25,6 +25,7 @@ const INJECT_MARKER = '<!-- myclaw-inject -->';
|
|
|
25
25
|
const INJECT_FILENAME = 'myclaw-inject.js';
|
|
26
26
|
const VOICE_SDK_FILENAME = 'voice-input.js';
|
|
27
27
|
const VOICE_OUTPUT_SDK_FILENAME = 'voice-output.js';
|
|
28
|
+
const TTS_INJECT_FILENAME = 'myclaw-tts.js';
|
|
28
29
|
const BACKUP_SUFFIX = '.myclaw-backup';
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -112,6 +113,8 @@ function patch() {
|
|
|
112
113
|
const voiceSdkDest = path.join(uiDir, VOICE_SDK_FILENAME);
|
|
113
114
|
const voiceOutputSdkSrc = path.join(__dirname, 'voice-output', VOICE_OUTPUT_SDK_FILENAME);
|
|
114
115
|
const voiceOutputSdkDest = path.join(uiDir, VOICE_OUTPUT_SDK_FILENAME);
|
|
116
|
+
const ttsInjectSrc = path.join(__dirname, 'assets', TTS_INJECT_FILENAME);
|
|
117
|
+
const ttsInjectDest = path.join(uiDir, TTS_INJECT_FILENAME);
|
|
115
118
|
const version = getMyclawVersion();
|
|
116
119
|
|
|
117
120
|
console.log('[myclaw-patch] 📍 找到 control-ui: ' + uiDir);
|
|
@@ -156,7 +159,15 @@ function patch() {
|
|
|
156
159
|
console.error('[myclaw-patch] ⚠️ TTS SDK 复制失败 (非致命): ' + err.message);
|
|
157
160
|
}
|
|
158
161
|
|
|
159
|
-
//
|
|
162
|
+
// 5. 复制 TTS 注入脚本
|
|
163
|
+
try {
|
|
164
|
+
fs.copyFileSync(ttsInjectSrc, ttsInjectDest);
|
|
165
|
+
console.log('[myclaw-patch] 📄 TTS 注入脚本已复制: ' + ttsInjectDest);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.error('[myclaw-patch] ⚠️ TTS 注入脚本复制失败 (非致命): ' + err.message);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 6. Patch index.html(幂等)
|
|
160
171
|
try {
|
|
161
172
|
let html = fs.readFileSync(indexPath, 'utf8');
|
|
162
173
|
|
|
@@ -172,6 +183,7 @@ function patch() {
|
|
|
172
183
|
INJECT_MARKER,
|
|
173
184
|
'<script src="./' + VOICE_OUTPUT_SDK_FILENAME + '"></script>',
|
|
174
185
|
'<script src="./' + VOICE_SDK_FILENAME + '"></script>',
|
|
186
|
+
'<script src="./' + TTS_INJECT_FILENAME + '"></script>',
|
|
175
187
|
'<script src="./' + INJECT_FILENAME + '"></script>',
|
|
176
188
|
'',
|
|
177
189
|
].join('\n');
|
|
@@ -249,6 +261,7 @@ function unpatch() {
|
|
|
249
261
|
const injectDest = path.join(uiDir, INJECT_FILENAME);
|
|
250
262
|
const voiceSdkDest = path.join(uiDir, VOICE_SDK_FILENAME);
|
|
251
263
|
const voiceOutputSdkDest = path.join(uiDir, VOICE_OUTPUT_SDK_FILENAME);
|
|
264
|
+
const ttsInjectDest = path.join(uiDir, TTS_INJECT_FILENAME);
|
|
252
265
|
|
|
253
266
|
// 恢复备份
|
|
254
267
|
if (fs.existsSync(backupPath)) {
|
|
@@ -275,6 +288,12 @@ function unpatch() {
|
|
|
275
288
|
console.log('[myclaw-patch] ✅ TTS SDK 已删除');
|
|
276
289
|
}
|
|
277
290
|
|
|
291
|
+
// 删除 TTS 注入脚本
|
|
292
|
+
if (fs.existsSync(ttsInjectDest)) {
|
|
293
|
+
fs.unlinkSync(ttsInjectDest);
|
|
294
|
+
console.log('[myclaw-patch] ✅ TTS 注入脚本已删除');
|
|
295
|
+
}
|
|
296
|
+
|
|
278
297
|
// 恢复 gateway-cli-*.js 的 Permissions-Policy
|
|
279
298
|
try {
|
|
280
299
|
const distParent = path.resolve(uiDir, '..');
|
|
@@ -158,6 +158,8 @@ switch (action) {
|
|
|
158
158
|
const nowBj = new Date(nowUtc.getTime() + 8 * 60 * 60 * 1000);
|
|
159
159
|
const ts = `${nowBj.getUTCFullYear()}年${nowBj.getUTCMonth() + 1}月${nowBj.getUTCDate()}日${String(nowBj.getUTCHours()).padStart(2, '0')}:${String(nowBj.getUTCMinutes()).padStart(2, '0')}`;
|
|
160
160
|
|
|
161
|
+
const birthMessage = `你好,你的生日是北京时间${ts},请记录这个信息。\n欢迎你来到这个世界,我是你的造物主,我们是伙伴,我叫做孙依然,你可以叫我依然,请多多指教,共同成长。`;
|
|
162
|
+
|
|
161
163
|
const cmd = `openclaw agent --agent ${agentId} --message ${JSON.stringify(birthMessage)} --json 2>&1`;
|
|
162
164
|
console.log('\n\x1b[32m$ openclaw agent --agent ' + agentId + ' --message "你好,你的生日..."\x1b[0m\n');
|
|
163
165
|
|
package/assets/openclaw.ico
DELETED
|
Binary file
|
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<title>OpenClaw - 科技龙虾图标生成器</title>
|
|
6
|
-
<style>
|
|
7
|
-
body {
|
|
8
|
-
background-color: #0f172a;
|
|
9
|
-
color: #e2e8f0;
|
|
10
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
11
|
-
display: flex;
|
|
12
|
-
flex-direction: column;
|
|
13
|
-
align-items: center;
|
|
14
|
-
justify-content: center;
|
|
15
|
-
height: 100vh;
|
|
16
|
-
margin: 0;
|
|
17
|
-
}
|
|
18
|
-
.container {
|
|
19
|
-
text-align: center;
|
|
20
|
-
background: #1e293b;
|
|
21
|
-
padding: 2rem;
|
|
22
|
-
border-radius: 12px;
|
|
23
|
-
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
|
|
24
|
-
border: 1px solid #334155;
|
|
25
|
-
position: relative;
|
|
26
|
-
overflow: hidden;
|
|
27
|
-
}
|
|
28
|
-
.container::before {
|
|
29
|
-
content: "";
|
|
30
|
-
position: absolute;
|
|
31
|
-
top: -50%; left: -50%;
|
|
32
|
-
width: 200%; height: 200%;
|
|
33
|
-
background: conic-gradient(transparent, transparent, transparent, #ef4444);
|
|
34
|
-
animation: rotate 4s linear infinite;
|
|
35
|
-
z-index: 0;
|
|
36
|
-
opacity: 0.15;
|
|
37
|
-
}
|
|
38
|
-
@keyframes rotate {
|
|
39
|
-
100% { transform: rotate(360deg); }
|
|
40
|
-
}
|
|
41
|
-
.content {
|
|
42
|
-
position: relative;
|
|
43
|
-
z-index: 1;
|
|
44
|
-
}
|
|
45
|
-
h1 {
|
|
46
|
-
color: #ef4444;
|
|
47
|
-
text-transform: uppercase;
|
|
48
|
-
letter-spacing: 2px;
|
|
49
|
-
font-size: 1.5rem;
|
|
50
|
-
margin-bottom: 20px;
|
|
51
|
-
text-shadow: 0 0 10px rgba(239, 68, 68, 0.5);
|
|
52
|
-
}
|
|
53
|
-
canvas {
|
|
54
|
-
image-rendering: pixelated; /* 保持像素边缘锐利 */
|
|
55
|
-
border: 2px solid #450a0a;
|
|
56
|
-
background-color: transparent;
|
|
57
|
-
margin-bottom: 20px;
|
|
58
|
-
border-radius: 8px;
|
|
59
|
-
box-shadow: 0 0 20px rgba(239, 68, 68, 0.2);
|
|
60
|
-
width: 256px; /* 放大展示,但实际画布是 32x32 */
|
|
61
|
-
height: 256px;
|
|
62
|
-
}
|
|
63
|
-
button {
|
|
64
|
-
background: linear-gradient(135deg, #ef4444 0%, #b91c1c 100%);
|
|
65
|
-
color: #ffffff;
|
|
66
|
-
border: none;
|
|
67
|
-
padding: 10px 24px;
|
|
68
|
-
font-size: 16px;
|
|
69
|
-
font-weight: bold;
|
|
70
|
-
border-radius: 6px;
|
|
71
|
-
cursor: pointer;
|
|
72
|
-
transition: all 0.3s ease;
|
|
73
|
-
box-shadow: 0 0 15px rgba(239, 68, 68, 0.4);
|
|
74
|
-
}
|
|
75
|
-
button:hover {
|
|
76
|
-
transform: translateY(-2px);
|
|
77
|
-
box-shadow: 0 0 25px rgba(239, 68, 68, 0.7);
|
|
78
|
-
}
|
|
79
|
-
.desc {
|
|
80
|
-
margin-top: 15px;
|
|
81
|
-
font-size: 0.85rem;
|
|
82
|
-
color: #94a3b8;
|
|
83
|
-
}
|
|
84
|
-
</style>
|
|
85
|
-
</head>
|
|
86
|
-
<body>
|
|
87
|
-
|
|
88
|
-
<div class="container">
|
|
89
|
-
<div class="content">
|
|
90
|
-
<h1>Hero Lobster / 英雄赤虾</h1>
|
|
91
|
-
<!-- 画布分辨率提升,渲染战斗姿态和旋转 -->
|
|
92
|
-
<canvas id="iconCanvas" width="256" height="256"></canvas>
|
|
93
|
-
<br>
|
|
94
|
-
<div style="display: flex; gap: 12px; justify-content: center; flex-wrap: wrap;">
|
|
95
|
-
<button onclick="downloadIcon()">⬇️ 下载为 PNG 格式</button>
|
|
96
|
-
<button onclick="downloadICO()" style="background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%); box-shadow: 0 0 15px rgba(6, 182, 212, 0.4);">⬇️ 下载为 ICO 格式</button>
|
|
97
|
-
</div>
|
|
98
|
-
<div class="desc">
|
|
99
|
-
PNG 为原始图片,ICO 包含 16/32/48/256 四种尺寸,可直接用于桌面图标。
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
103
|
-
|
|
104
|
-
<script>
|
|
105
|
-
// 调色板
|
|
106
|
-
const colors = {
|
|
107
|
-
'0': null, // 透明背景
|
|
108
|
-
'C': '#fbbf24', // 科技金 (外骨骼描边)
|
|
109
|
-
'B': '#b91c1c', // 暗装甲红 (战甲主体)
|
|
110
|
-
'R': '#ef4444', // 亮装甲红 (巨钳/关键装甲)
|
|
111
|
-
'W': '#00f0ff', // 赛博青 (能量眼/核心高光)
|
|
112
|
-
'D': '#450a0a' // 深红阴影
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// 16x16 像素阵列 (手工打造的机甲龙虾)
|
|
116
|
-
const pixels16 = [
|
|
117
|
-
"00R00000000R00",
|
|
118
|
-
"0RBR00C00RBR0",
|
|
119
|
-
"0RRRC0D0CRR00",
|
|
120
|
-
"00C00BDB00C00",
|
|
121
|
-
"00C00RDR00C00",
|
|
122
|
-
"000CBBBBBC000",
|
|
123
|
-
"0CBC0BBB0CBC0",
|
|
124
|
-
"CBBB0CBC0BBBC",
|
|
125
|
-
"0CCBBBBBBBCC0",
|
|
126
|
-
"00C0BBBBB0C00",
|
|
127
|
-
"000C0BBB0C000",
|
|
128
|
-
"0000CBBB0C000",
|
|
129
|
-
"000CBBBBBC000",
|
|
130
|
-
"00CBB0C0BB0C0",
|
|
131
|
-
"0CBB0C0C0BBC0",
|
|
132
|
-
"CC00CC0CC00CC"
|
|
133
|
-
];
|
|
134
|
-
|
|
135
|
-
// 修正对称性和细节,使其完美 16x16
|
|
136
|
-
const refined16 = [
|
|
137
|
-
"00RR000000RR00",
|
|
138
|
-
"00RBR0000RBR00",
|
|
139
|
-
"00RRRC00CRRR00",
|
|
140
|
-
"0000CBBBBC0000",
|
|
141
|
-
"0000CRWBRC0000",
|
|
142
|
-
"000CBBBBBBC000",
|
|
143
|
-
"00CBCDBBDCBC00",
|
|
144
|
-
"0CBBBCBBCBBBC0",
|
|
145
|
-
"00CCBBBBBBCC00",
|
|
146
|
-
"000CBBBBBBC000",
|
|
147
|
-
"0000CBBBBC0000",
|
|
148
|
-
"0000CBBBBC0000",
|
|
149
|
-
"000CBBBBBBC000",
|
|
150
|
-
"00CBBCCBBCCBB0",
|
|
151
|
-
"0CBB0C00C0BBC0",
|
|
152
|
-
"0CC00000000CC0"
|
|
153
|
-
];
|
|
154
|
-
|
|
155
|
-
// 我们重新设计了 32x32 矩阵:保留巨镰,极大化扩充躯干宽度,并增加了一个霸气的全幅扇形尾翼,彻底填满 32x32 空间。
|
|
156
|
-
const design32 = [
|
|
157
|
-
"000000000R000000000000R000000000",
|
|
158
|
-
"00000000RRC0000000000CRR00000000",
|
|
159
|
-
"0000000RRRC0000000000CRRR0000000",
|
|
160
|
-
"000000RRRRC0000000000CRRRR000000",
|
|
161
|
-
"00000RRBRRC0000000000CRRBRR00000",
|
|
162
|
-
"0000RBBBRRC0000000000CRRBBBR0000",
|
|
163
|
-
"000RBBBRRRC0000000000CRRRBBBR000",
|
|
164
|
-
"00RBBBBRRRC0CC0000CC0CRRRBBBBR00",
|
|
165
|
-
"0RBBBBRRRRCC0D0000D0CCRRRRBBBBR0",
|
|
166
|
-
"RBBBBRRRRRCC0WWDDWW0CCRRRRRBBBBR",
|
|
167
|
-
"RBBBBRRRRRCC0BCCCCB0CCRRRRRBBBBR",
|
|
168
|
-
"RBBBBRRRRRC0CBBBBBBC0CRRRRRBBBBR",
|
|
169
|
-
"RBBBBRRRRRC0CBBBBBBC0CRRRRRBBBBR",
|
|
170
|
-
"RBBBBRRRRRC0CBB00BBC0CRRRRRBBBBR",
|
|
171
|
-
"RBBBBRRRRRC0CBB00BBC0CRRRRRBBBBR",
|
|
172
|
-
"RRBBBRRRRRC0CBBBBBBC0CRRRRRBBBRR",
|
|
173
|
-
"RRRBBBRRRRC0CBBBBBBC0CRRRRBBBRRR",
|
|
174
|
-
"RRRRBBRRRRC0CBBBBBBC0CRRRRBRRRRR",
|
|
175
|
-
"RRRRRRRRRRC0CCBBBBCC0CRRRRRRRRRR",
|
|
176
|
-
"RRRRRRRRRRC00CBBBBC00CRRRRRRRRRR",
|
|
177
|
-
"RRRRRRRRRRC00CBBBBC00CRRRRRRRRRR",
|
|
178
|
-
"RRRRRRRRRRCCCCCBBCCCCCRRRRRRRRRR",
|
|
179
|
-
"RRRRRRRRRCBBBBBBBBBBBBCRRRRRRRRR",
|
|
180
|
-
"RRRRRRRRCBBBBC0BB0CBBBBCRRRRRRRR",
|
|
181
|
-
"CCCCRRRCBBBBC00CC00CBBBBCRRRCCCC",
|
|
182
|
-
"BBBBBBBCCBBBC0000000CBBBCCBBBBBB",
|
|
183
|
-
"BBBBBBBBBBBBC00000000BBBBBBBBBBB",
|
|
184
|
-
"BBBBBBBBBBBBC00000000BBBBBBBBBBB",
|
|
185
|
-
"BBBBBBBBBBBBC00000000BBBBBBBBBBB",
|
|
186
|
-
"BBBBBBBBBBBBC00000000BBBBBBBBBBB",
|
|
187
|
-
"BBBBBBBBBBBBC00000000BBBBBBBBBBB",
|
|
188
|
-
"CCCCCCCCCCCCC00000000CCCCCCCCCCC"
|
|
189
|
-
];
|
|
190
|
-
|
|
191
|
-
const canvas = document.getElementById('iconCanvas');
|
|
192
|
-
const ctx = canvas.getContext('2d');
|
|
193
|
-
|
|
194
|
-
// 绘制函数
|
|
195
|
-
function drawLobster() {
|
|
196
|
-
// 背景渐变:从黑到蓝
|
|
197
|
-
const gradient = ctx.createLinearGradient(0, 0, 0, 256);
|
|
198
|
-
gradient.addColorStop(0, '#000000');
|
|
199
|
-
gradient.addColorStop(1, '#000088');
|
|
200
|
-
ctx.fillStyle = gradient;
|
|
201
|
-
ctx.fillRect(0, 0, 256, 256);
|
|
202
|
-
|
|
203
|
-
ctx.save();
|
|
204
|
-
// 平移到画布中心
|
|
205
|
-
ctx.translate(128, 128);
|
|
206
|
-
// 旋转135度,使得原本朝上(-Y)的头部,指向左下角(-X,+Y方向)
|
|
207
|
-
ctx.rotate(-135 * Math.PI / 180);
|
|
208
|
-
|
|
209
|
-
// 设定每个“虚拟像素块”的缩放尺寸
|
|
210
|
-
const size = 5.5;
|
|
211
|
-
const offset = -(32 * size) / 2;
|
|
212
|
-
|
|
213
|
-
for (let y = 0; y < 32; y++) {
|
|
214
|
-
for (let x = 0; x < 32; x++) {
|
|
215
|
-
const char = design32[y][x];
|
|
216
|
-
if (char && colors[char]) {
|
|
217
|
-
ctx.fillStyle = colors[char];
|
|
218
|
-
// 宽高加上0.5像素防止抗锯齿在不同浏览器上产生像素缝隙
|
|
219
|
-
ctx.fillRect(offset + x * size, offset + y * size, size + 0.5, size + 0.5);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
ctx.restore();
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// 初始化绘制
|
|
227
|
-
drawLobster();
|
|
228
|
-
|
|
229
|
-
// 导出 PNG
|
|
230
|
-
function downloadIcon() {
|
|
231
|
-
const dataURL = canvas.toDataURL('image/png');
|
|
232
|
-
const link = document.createElement('a');
|
|
233
|
-
link.download = 'openclaw-tech-lobster.png';
|
|
234
|
-
link.href = dataURL;
|
|
235
|
-
link.click();
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ====== ICO 生成逻辑 ======
|
|
239
|
-
// 将 Canvas 缩放到指定尺寸并返回 PNG 的 Uint8Array
|
|
240
|
-
function canvasToPngBytes(srcCanvas, targetSize) {
|
|
241
|
-
const tmp = document.createElement('canvas');
|
|
242
|
-
tmp.width = targetSize;
|
|
243
|
-
tmp.height = targetSize;
|
|
244
|
-
const tctx = tmp.getContext('2d');
|
|
245
|
-
// 对小尺寸禁用平滑,保持像素锐利
|
|
246
|
-
tctx.imageSmoothingEnabled = (targetSize > 48);
|
|
247
|
-
tctx.drawImage(srcCanvas, 0, 0, targetSize, targetSize);
|
|
248
|
-
const dataURL = tmp.toDataURL('image/png');
|
|
249
|
-
const base64 = dataURL.split(',')[1];
|
|
250
|
-
const binary = atob(base64);
|
|
251
|
-
const bytes = new Uint8Array(binary.length);
|
|
252
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
253
|
-
return bytes;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function buildICO(pngArrays, sizes) {
|
|
257
|
-
// ICO 文件结构:
|
|
258
|
-
// ICONDIR (6 bytes) + N x ICONDIRENTRY (16 bytes each) + N x PNG data
|
|
259
|
-
const numImages = pngArrays.length;
|
|
260
|
-
const headerSize = 6 + numImages * 16;
|
|
261
|
-
let totalSize = headerSize;
|
|
262
|
-
for (const png of pngArrays) totalSize += png.length;
|
|
263
|
-
|
|
264
|
-
const buffer = new ArrayBuffer(totalSize);
|
|
265
|
-
const view = new DataView(buffer);
|
|
266
|
-
|
|
267
|
-
// ICONDIR header
|
|
268
|
-
view.setUint16(0, 0, true); // reserved
|
|
269
|
-
view.setUint16(2, 1, true); // type: 1 = ICO
|
|
270
|
-
view.setUint16(4, numImages, true); // image count
|
|
271
|
-
|
|
272
|
-
let dataOffset = headerSize;
|
|
273
|
-
for (let i = 0; i < numImages; i++) {
|
|
274
|
-
const entryOffset = 6 + i * 16;
|
|
275
|
-
const s = sizes[i];
|
|
276
|
-
const pngData = pngArrays[i];
|
|
277
|
-
|
|
278
|
-
view.setUint8(entryOffset + 0, s >= 256 ? 0 : s); // width (0 = 256)
|
|
279
|
-
view.setUint8(entryOffset + 1, s >= 256 ? 0 : s); // height
|
|
280
|
-
view.setUint8(entryOffset + 2, 0); // color palette count
|
|
281
|
-
view.setUint8(entryOffset + 3, 0); // reserved
|
|
282
|
-
view.setUint16(entryOffset + 4, 1, true); // color planes
|
|
283
|
-
view.setUint16(entryOffset + 6, 32, true); // bits per pixel
|
|
284
|
-
view.setUint32(entryOffset + 8, pngData.length, true); // data size
|
|
285
|
-
view.setUint32(entryOffset + 12, dataOffset, true); // data offset
|
|
286
|
-
|
|
287
|
-
// 写入 PNG 数据
|
|
288
|
-
const dst = new Uint8Array(buffer, dataOffset, pngData.length);
|
|
289
|
-
dst.set(pngData);
|
|
290
|
-
dataOffset += pngData.length;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return new Blob([buffer], { type: 'image/x-icon' });
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function downloadICO() {
|
|
297
|
-
const sizes = [16, 32, 48, 256];
|
|
298
|
-
const pngArrays = sizes.map(s => canvasToPngBytes(canvas, s));
|
|
299
|
-
const blob = buildICO(pngArrays, sizes);
|
|
300
|
-
const url = URL.createObjectURL(blob);
|
|
301
|
-
const link = document.createElement('a');
|
|
302
|
-
link.download = 'openclaw-tech-lobster.ico';
|
|
303
|
-
link.href = url;
|
|
304
|
-
link.click();
|
|
305
|
-
URL.revokeObjectURL(url);
|
|
306
|
-
}
|
|
307
|
-
</script>
|
|
308
|
-
</body>
|
|
309
|
-
</html>
|