@aiyiran/myclaw 1.0.103 → 1.0.105
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/index.js +170 -3
- package/package.json +1 -1
- package/patch-manifest.json +1 -1
- package/patch.js +20 -1
- package/voice-output/voice-output.js +24 -0
- package/wizards/scripts/create_agent_steps.js +2 -0
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: 'aisbabyxu',
|
|
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/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
|
// ============================================================================
|
|
@@ -776,7 +943,7 @@ function showHelp() {
|
|
|
776
943
|
console.log('');
|
|
777
944
|
console.log('向导 (交互式):');
|
|
778
945
|
console.log(' prepare 智能初始化(环境检测 + 安装 + Patch)');
|
|
779
|
-
console.log(' new 创建新 Agent
|
|
946
|
+
console.log(' new 创建新 Agent(无参数=向导,有参数=一键)');
|
|
780
947
|
console.log(' weixin 微信接入向导(步骤引导 + 教学说明)');
|
|
781
948
|
console.log(' rebind 微信换绑向导(清空、扫码、换Agent)');
|
|
782
949
|
console.log('');
|
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, '..');
|
|
@@ -107,9 +107,33 @@
|
|
|
107
107
|
VoiceOutput.prototype._playAudio = function (audioBuffer) {
|
|
108
108
|
var self = this;
|
|
109
109
|
|
|
110
|
+
// 尝试 Web Audio API(可以解码 MP3)
|
|
111
|
+
try {
|
|
112
|
+
this._initAudioContext();
|
|
113
|
+
this.audioContext.decodeAudioData(audioBuffer.slice(0), function (decodedBuffer) {
|
|
114
|
+
console.log('[tts] decoded audio:', decodedBuffer.duration, 's,', decodedBuffer.numberOfChannels, 'ch');
|
|
115
|
+
self._playBuffer(decodedBuffer);
|
|
116
|
+
}, function (err) {
|
|
117
|
+
console.error('[tts] decode error, trying blob URL:', err);
|
|
118
|
+
// fallback: 用 Blob URL 方式
|
|
119
|
+
self._playAudioBlob(audioBuffer);
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
} catch (e) {
|
|
123
|
+
console.error('[tts] Web Audio API error:', e);
|
|
124
|
+
// fallback: 用 Blob URL 方式
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this._playAudioBlob(audioBuffer);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
VoiceOutput.prototype._playAudioBlob = function (audioBuffer) {
|
|
131
|
+
var self = this;
|
|
132
|
+
|
|
110
133
|
// 创建 Blob URL 用于 <audio> 元素播放
|
|
111
134
|
var mimeType = this.aue === 'lame' ? 'audio/mpeg' : 'audio/wav';
|
|
112
135
|
var blob = new Blob([audioBuffer], { type: mimeType });
|
|
136
|
+
console.log('[tts] blob size:', blob.size, 'type:', mimeType);
|
|
113
137
|
var blobUrl = URL.createObjectURL(blob);
|
|
114
138
|
|
|
115
139
|
// 停止之前的播放
|
|
@@ -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
|
|