@aiyiran/myclaw 1.0.123 → 1.0.125
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-tts.js +159 -157
- package/package.json +1 -1
- package/patch-manifest.json +1 -1
- package/wsl2.js +83 -80
package/assets/myclaw-tts.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* MyClaw TTS — 讯飞语音合成
|
|
4
4
|
* ============================================================================
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 功能:在每条消息的 .chat-group-footer 左侧注入播放按钮,用文字显示状态
|
|
7
7
|
*
|
|
8
8
|
* 状态:
|
|
9
|
-
* idle
|
|
10
|
-
* generating
|
|
11
|
-
* playing
|
|
9
|
+
* idle → 默认,显示"播放"
|
|
10
|
+
* generating → 合成中,显示"生成中"
|
|
11
|
+
* playing → 播放中,显示"播放中",点击停止并回到"播放"
|
|
12
12
|
*
|
|
13
13
|
* 依赖:voice-output.js(讯飞 VoiceOutput SDK,需先于本脚本加载)
|
|
14
14
|
* ============================================================================
|
|
@@ -18,19 +18,18 @@
|
|
|
18
18
|
|
|
19
19
|
// ═══ 状态 ═══
|
|
20
20
|
var tts = null; // VoiceOutput 实例
|
|
21
|
-
var ttsBtnHooked = false;
|
|
22
|
-
var currentBtn = null; // 当前正在播放的按钮
|
|
23
21
|
var playerState = 'idle'; // idle | generating | playing
|
|
22
|
+
var currentBtn = null; // 当前正在使用的按钮
|
|
24
23
|
|
|
25
24
|
// ═══ 讯飞配置 ═══
|
|
26
25
|
var IFLYTEK_CONFIG = {
|
|
27
26
|
APPID: 'de6ec59e',
|
|
28
27
|
APISecret: 'ZTA3ZDU3YjAyNDUyZGVkNjQwMGM5MWNi',
|
|
29
28
|
APIKey: '37104a0463a5460d7571869324b667d5',
|
|
30
|
-
vcn: 'x4_yezi',
|
|
29
|
+
vcn: 'x4_yezi',
|
|
31
30
|
};
|
|
32
31
|
|
|
33
|
-
// ═══
|
|
32
|
+
// ═══ 公开接口 ═══
|
|
34
33
|
window.MyClawTts = {
|
|
35
34
|
setVcn: function (vcn) {
|
|
36
35
|
IFLYTEK_CONFIG.vcn = vcn;
|
|
@@ -45,53 +44,139 @@
|
|
|
45
44
|
},
|
|
46
45
|
};
|
|
47
46
|
|
|
48
|
-
// ═══
|
|
49
|
-
var
|
|
50
|
-
idle: '
|
|
51
|
-
generating: '
|
|
52
|
-
playing: '
|
|
47
|
+
// ═══ 状态文字 ═══
|
|
48
|
+
var STATE_TEXT = {
|
|
49
|
+
idle: '播放',
|
|
50
|
+
generating: '生成中',
|
|
51
|
+
playing: '播放中',
|
|
53
52
|
};
|
|
54
53
|
|
|
55
|
-
// ═══
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
// ═══ 提取消息文字 ═══
|
|
55
|
+
function extractMessageText(btn) {
|
|
56
|
+
var group = btn.closest('.chat-group-messages');
|
|
57
|
+
if (!group) return '';
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (svg) {
|
|
63
|
-
svg.style.display = state === 'idle' ? '' : 'none';
|
|
64
|
-
}
|
|
59
|
+
var bubble = group.querySelector('.chat-bubble');
|
|
60
|
+
if (!bubble) return '';
|
|
65
61
|
|
|
66
|
-
|
|
62
|
+
var chatText = bubble.querySelector('.chat-text');
|
|
63
|
+
if (!chatText) return '';
|
|
64
|
+
|
|
65
|
+
var text = chatText.textContent || '';
|
|
66
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ═══ 创建按钮 ═══
|
|
70
|
+
function createTtsBtn() {
|
|
71
|
+
var btn = document.createElement('button');
|
|
72
|
+
btn.className = 'myclaw-tts-btn';
|
|
73
|
+
btn.type = 'button';
|
|
74
|
+
btn.textContent = STATE_TEXT.idle;
|
|
75
|
+
return btn;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ═══ 更新按钮状态 ═══
|
|
79
|
+
function updateBtn(btn, state) {
|
|
80
|
+
if (!btn) return;
|
|
81
|
+
playerState = state;
|
|
82
|
+
btn.textContent = STATE_TEXT[state];
|
|
67
83
|
btn.classList.remove('myclaw-tts--generating', 'myclaw-tts--playing');
|
|
68
84
|
|
|
69
85
|
if (state === 'generating') {
|
|
70
86
|
btn.classList.add('myclaw-tts--generating');
|
|
71
|
-
btn.title = '生成中...';
|
|
72
87
|
} else if (state === 'playing') {
|
|
73
88
|
btn.classList.add('myclaw-tts--playing');
|
|
74
|
-
btn.title = '点击停止';
|
|
75
|
-
} else {
|
|
76
|
-
btn.title = '朗读';
|
|
77
89
|
}
|
|
78
90
|
}
|
|
79
91
|
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
// ═══ 注入按钮到 footer ═══
|
|
93
|
+
function injectBtn(footer) {
|
|
94
|
+
// 避免重复注入
|
|
95
|
+
if (footer.querySelector('.myclaw-tts-btn')) return;
|
|
96
|
+
|
|
97
|
+
var btn = createTtsBtn();
|
|
98
|
+
|
|
99
|
+
btn.addEventListener('click', function (e) {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
e.stopPropagation();
|
|
82
102
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (oldEmoji) oldEmoji.remove();
|
|
103
|
+
var state = playerState;
|
|
104
|
+
var text = extractMessageText(btn);
|
|
86
105
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
emoji.style.cssText = 'display:inline-block;width:18px;height:18px;line-height:18px;text-align:center;';
|
|
106
|
+
if (!text) {
|
|
107
|
+
console.warn('[myclaw-tts] 未找到消息文字');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
92
110
|
|
|
93
|
-
|
|
94
|
-
|
|
111
|
+
// 正在播放时,点击停止
|
|
112
|
+
if (state === 'playing' && currentBtn === btn) {
|
|
113
|
+
console.log('[myclaw-tts] 停止播放');
|
|
114
|
+
if (tts) tts.stop();
|
|
115
|
+
updateBtn(btn, 'idle');
|
|
116
|
+
currentBtn = null;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 已有其他按钮在播放,先停掉
|
|
121
|
+
if (state === 'playing' && currentBtn && currentBtn !== btn) {
|
|
122
|
+
if (tts) tts.stop();
|
|
123
|
+
updateBtn(currentBtn, 'idle');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log('[myclaw-tts] 播放:', text.substring(0, 50) + (text.length > 50 ? '...' : ''));
|
|
127
|
+
|
|
128
|
+
currentBtn = btn;
|
|
129
|
+
updateBtn(btn, 'generating');
|
|
130
|
+
|
|
131
|
+
if (!tts) initTts();
|
|
132
|
+
if (tts) {
|
|
133
|
+
tts.onAudioEnd = function () {
|
|
134
|
+
console.log('[myclaw-tts] 播放结束');
|
|
135
|
+
if (currentBtn) {
|
|
136
|
+
updateBtn(currentBtn, 'idle');
|
|
137
|
+
currentBtn = null;
|
|
138
|
+
}
|
|
139
|
+
playerState = 'idle';
|
|
140
|
+
};
|
|
141
|
+
tts.vcn = IFLYTEK_CONFIG.vcn;
|
|
142
|
+
tts.speak(text);
|
|
143
|
+
|
|
144
|
+
// 等待合成完成进入播放状态
|
|
145
|
+
setTimeout(function () {
|
|
146
|
+
if (playerState === 'generating') {
|
|
147
|
+
updateBtn(btn, 'playing');
|
|
148
|
+
}
|
|
149
|
+
}, 150);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// 插入到 footer 最前面(左边),原有按钮保留在右边
|
|
154
|
+
footer.insertBefore(btn, footer.firstChild);
|
|
155
|
+
console.log('[myclaw-tts] ✅ 播放按钮已注入');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ═══ 观察 footer 变化 ═══
|
|
159
|
+
function observeFooters() {
|
|
160
|
+
new MutationObserver(function (mutations) {
|
|
161
|
+
mutations.forEach(function (mutation) {
|
|
162
|
+
mutation.addedNodes.forEach(function (node) {
|
|
163
|
+
if (node.nodeType !== 1) return;
|
|
164
|
+
|
|
165
|
+
// 直接是 footer
|
|
166
|
+
if (node.classList && node.classList.contains('chat-group-footer')) {
|
|
167
|
+
injectBtn(node);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// footer 内部的子节点
|
|
171
|
+
if (node.querySelectorAll) {
|
|
172
|
+
node.querySelectorAll('.chat-group-footer').forEach(injectBtn);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}).observe(document.documentElement, { childList: true, subtree: true });
|
|
177
|
+
|
|
178
|
+
// 立即处理已有的 footer
|
|
179
|
+
document.querySelectorAll('.chat-group-footer').forEach(injectBtn);
|
|
95
180
|
}
|
|
96
181
|
|
|
97
182
|
// ═══ 初始化 TTS ═══
|
|
@@ -107,32 +192,21 @@
|
|
|
107
192
|
APISecret: IFLYTEK_CONFIG.APISecret,
|
|
108
193
|
APIKey: IFLYTEK_CONFIG.APIKey,
|
|
109
194
|
vcn: IFLYTEK_CONFIG.vcn,
|
|
110
|
-
onAudio: function (audioBuffer) {
|
|
111
|
-
// console.log('[myclaw-tts] audio chunk:', audioBuffer.byteLength, 'bytes');
|
|
112
|
-
},
|
|
113
195
|
onStatusChange: function (oldStatus, newStatus) {
|
|
114
196
|
console.log('[myclaw-tts] status:', oldStatus, '->', newStatus);
|
|
115
197
|
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
setPlayerState(currentBtn, 'generating');
|
|
121
|
-
} else if (newStatus === 'idle') {
|
|
122
|
-
if (playerState === 'generating') {
|
|
123
|
-
// 合成完成但未开始播放(可能是空文字或其他错误)
|
|
124
|
-
setPlayerState(currentBtn, 'idle');
|
|
125
|
-
}
|
|
126
|
-
// playing 状态会在 audio.onended 时处理
|
|
198
|
+
if (newStatus === 'idle' && playerState === 'generating') {
|
|
199
|
+
// 合成失败或空内容
|
|
200
|
+
if (currentBtn) updateBtn(currentBtn, 'idle');
|
|
201
|
+
currentBtn = null;
|
|
127
202
|
}
|
|
128
203
|
},
|
|
129
204
|
onError: function (err) {
|
|
130
205
|
console.error('[myclaw-tts] error:', err);
|
|
131
206
|
if (currentBtn) {
|
|
132
|
-
|
|
133
|
-
|
|
207
|
+
updateBtn(currentBtn, 'idle');
|
|
208
|
+
currentBtn = null;
|
|
134
209
|
}
|
|
135
|
-
currentBtn = null;
|
|
136
210
|
playerState = 'idle';
|
|
137
211
|
},
|
|
138
212
|
});
|
|
@@ -140,41 +214,41 @@
|
|
|
140
214
|
console.log('[myclaw-tts] ✅ 讯飞 TTS SDK 已初始化');
|
|
141
215
|
}
|
|
142
216
|
|
|
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
|
-
// ═══ 注入样式 ═══
|
|
217
|
+
// ═══ 样式 ═══
|
|
159
218
|
function injectStyles() {
|
|
160
219
|
if (document.querySelector('#myclaw-tts-styles')) return;
|
|
161
220
|
var style = document.createElement('style');
|
|
162
221
|
style.id = 'myclaw-tts-styles';
|
|
163
222
|
style.textContent = [
|
|
164
|
-
/*
|
|
165
|
-
'.myclaw-tts
|
|
166
|
-
'
|
|
167
|
-
'
|
|
223
|
+
/* 按钮基础样式 */
|
|
224
|
+
'.myclaw-tts-btn {',
|
|
225
|
+
' background: rgba(59, 130, 246, 0.1);',
|
|
226
|
+
' border: 1px solid rgba(59, 130, 246, 0.3);',
|
|
227
|
+
' border-radius: 4px;',
|
|
228
|
+
' color: #3b82f6;',
|
|
229
|
+
' font-size: 12px;',
|
|
230
|
+
' padding: 2px 8px;',
|
|
231
|
+
' cursor: pointer;',
|
|
232
|
+
' transition: all 0.2s;',
|
|
233
|
+
' white-space: nowrap;',
|
|
234
|
+
'}',
|
|
235
|
+
'.myclaw-tts-btn:hover {',
|
|
236
|
+
' background: rgba(59, 130, 246, 0.2);',
|
|
168
237
|
'}',
|
|
169
|
-
|
|
170
|
-
'
|
|
171
|
-
'
|
|
238
|
+
/* 生成中 */
|
|
239
|
+
'.myclaw-tts-btn.myclaw-tts--generating {',
|
|
240
|
+
' color: #f59e0b;',
|
|
241
|
+
' border-color: rgba(245, 158, 11, 0.4);',
|
|
242
|
+
' background: rgba(245, 158, 11, 0.1);',
|
|
243
|
+
' animation: myclaw-tts-blink 0.8s ease-in-out infinite;',
|
|
172
244
|
'}',
|
|
173
|
-
/*
|
|
174
|
-
'.myclaw-tts--playing {',
|
|
175
|
-
'
|
|
245
|
+
/* 播放中 */
|
|
246
|
+
'.myclaw-tts-btn.myclaw-tts--playing {',
|
|
247
|
+
' color: #10b981;',
|
|
248
|
+
' border-color: rgba(16, 185, 129, 0.4);',
|
|
249
|
+
' background: rgba(16, 185, 129, 0.1);',
|
|
176
250
|
'}',
|
|
177
|
-
'@keyframes myclaw-tts-
|
|
251
|
+
'@keyframes myclaw-tts-blink {',
|
|
178
252
|
' 0%, 100% { opacity: 1; }',
|
|
179
253
|
' 50% { opacity: 0.5; }',
|
|
180
254
|
'}',
|
|
@@ -182,84 +256,12 @@
|
|
|
182
256
|
document.head.appendChild(style);
|
|
183
257
|
}
|
|
184
258
|
|
|
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
|
// ═══ 启动 ═══
|
|
259
260
|
function init() {
|
|
260
261
|
injectStyles();
|
|
261
262
|
initTts();
|
|
262
|
-
|
|
263
|
+
observeFooters();
|
|
264
|
+
console.log('[myclaw-tts] ✅ 初始化完成');
|
|
263
265
|
}
|
|
264
266
|
|
|
265
267
|
if (document.readyState === 'loading') {
|
package/package.json
CHANGED
package/patch-manifest.json
CHANGED
package/wsl2.js
CHANGED
|
@@ -5,20 +5,18 @@
|
|
|
5
5
|
* MyClaw WSL2 一键安装器
|
|
6
6
|
* ============================================================================
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* 采用“固定流程 + 状态标记”机制,避免使用容易出错的环境嗅探(wsl --status)。
|
|
9
|
+
* 标记文件:~/.myclaw_wsl_state
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - 没有?直接回车,从 CDN 下载
|
|
13
|
-
*
|
|
14
|
-
* 自动检测安装状态,分两阶段完成:
|
|
15
|
-
* Phase 1: 启用底层 Windows 功能 + 安装 WSL → 需要重启
|
|
16
|
-
* Phase 2: 导入预制 Linux 环境 → 自动启动 Gateway
|
|
11
|
+
* Phase 1: 启用底层 Windows 功能 + 安装 WSL → 需要重启
|
|
12
|
+
* Phase 2: 导入预制 Linux 环境 → 自动启动 Gateway
|
|
17
13
|
* ============================================================================
|
|
18
14
|
*/
|
|
19
15
|
|
|
20
16
|
const { execSync } = require('child_process');
|
|
21
17
|
const os = require('os');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
22
20
|
|
|
23
21
|
// ============================================================================
|
|
24
22
|
// 配置
|
|
@@ -35,14 +33,24 @@ const C = isWindows
|
|
|
35
33
|
? { r: '', g: '', y: '', b: '', nc: '' }
|
|
36
34
|
: { r: '\x1b[31m', g: '\x1b[32m', y: '\x1b[33m', b: '\x1b[34m', nc: '\x1b[0m' };
|
|
37
35
|
|
|
36
|
+
const STATE_FILE = path.join(os.homedir(), '.myclaw_wsl_state');
|
|
37
|
+
|
|
38
38
|
// ============================================================================
|
|
39
39
|
// 工具函数
|
|
40
40
|
// ============================================================================
|
|
41
41
|
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
function getState() {
|
|
43
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
44
|
+
return fs.readFileSync(STATE_FILE, 'utf8').trim();
|
|
45
|
+
}
|
|
46
|
+
return 'needs-features';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function setState(state) {
|
|
50
|
+
fs.writeFileSync(STATE_FILE, state, 'utf8');
|
|
51
|
+
}
|
|
45
52
|
|
|
53
|
+
function launchElevatedPS(script) {
|
|
46
54
|
// 把脚本写到临时文件(避免 Windows 命令行长度限制)
|
|
47
55
|
const tmpDir = process.env.LOCALAPPDATA
|
|
48
56
|
? path.join(process.env.LOCALAPPDATA, 'myclaw')
|
|
@@ -50,7 +58,7 @@ function launchElevatedPS(script) {
|
|
|
50
58
|
try { fs.mkdirSync(tmpDir, { recursive: true }); } catch {}
|
|
51
59
|
const scriptPath = path.join(tmpDir, 'wsl2_installer.ps1');
|
|
52
60
|
|
|
53
|
-
// UTF-8 with BOM
|
|
61
|
+
// UTF-8 with BOM
|
|
54
62
|
const BOM = '\uFEFF';
|
|
55
63
|
fs.writeFileSync(scriptPath, BOM + script, 'utf8');
|
|
56
64
|
|
|
@@ -68,14 +76,6 @@ function launchElevatedPS(script) {
|
|
|
68
76
|
}
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
/**
|
|
72
|
-
* 生成「拖拽 / 盘符搜索 / 回车」的 PowerShell 交互代码片段
|
|
73
|
-
* @param {string} prompt 提示文字
|
|
74
|
-
* @param {string} destVar 目标文件变量名(如 $msi, $tarPath)
|
|
75
|
-
* @param {string} cdnUrl CDN 下载地址
|
|
76
|
-
* @param {string} desc 文件描述(如 "WSL 安装包")
|
|
77
|
-
* @param {string} fileName 要搜索的文件名(如 "wsl_full.msi")
|
|
78
|
-
*/
|
|
79
79
|
function makeAskLocalOrCDN(prompt, destVar, cdnUrl, desc, fileName) {
|
|
80
80
|
return `
|
|
81
81
|
Write-Host ''
|
|
@@ -92,7 +92,6 @@ $userInput = $userInput.Trim().Trim('"')
|
|
|
92
92
|
$resolved = ''
|
|
93
93
|
|
|
94
94
|
if ($userInput) {
|
|
95
|
-
# 判断是否为单个盘符 (如 F, G, d)
|
|
96
95
|
if ($userInput -match '^[a-zA-Z]$') {
|
|
97
96
|
$drive = $userInput.ToUpper()
|
|
98
97
|
$drivePath = $drive + ':'
|
|
@@ -105,10 +104,8 @@ if ($userInput) {
|
|
|
105
104
|
Write-Host " 未在 $drivePath 中找到 ${fileName}"
|
|
106
105
|
}
|
|
107
106
|
} elseif (Test-Path $userInput -PathType Leaf) {
|
|
108
|
-
# 直接就是文件路径(拖拽)
|
|
109
107
|
$resolved = $userInput
|
|
110
108
|
} elseif (Test-Path $userInput -PathType Container) {
|
|
111
|
-
# 是目录,在里面搜索
|
|
112
109
|
Write-Host " 正在搜索 $userInput 中的 ${fileName}..."
|
|
113
110
|
$found = Get-ChildItem -Path $userInput -Filter '${fileName}' -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
114
111
|
if ($found) {
|
|
@@ -130,7 +127,13 @@ if ($resolved) {
|
|
|
130
127
|
if ($userInput) {
|
|
131
128
|
Write-Host ' 将改为从网络下载...'
|
|
132
129
|
}
|
|
133
|
-
Write-Host '
|
|
130
|
+
Write-Host ''
|
|
131
|
+
Write-Host ' ============================================================='
|
|
132
|
+
Write-Host ' [防卡住警告] 正在从网络下载,带有进度条,请耐心等待文件较大。'
|
|
133
|
+
Write-Host ' ⚠️ 请切勿使用鼠标点击此窗口内部!'
|
|
134
|
+
Write-Host ' ⚠️ 在 Windows 的机制下,只要用鼠标点一下窗口,下载就会被瞬间暂停冻结!'
|
|
135
|
+
Write-Host ' ⚠️ 如果你觉得进度条卡住不动了,请在键盘上按一下【回车键】即可解冻!'
|
|
136
|
+
Write-Host ' ============================================================='
|
|
134
137
|
try {
|
|
135
138
|
Start-BitsTransfer -Source '${cdnUrl}' -Destination ${destVar} -Description '下载 OpenClaw 核心镜像' -DisplayName 'OpenClaw'
|
|
136
139
|
Write-Host ' 下载完成!'
|
|
@@ -142,35 +145,17 @@ if ($resolved) {
|
|
|
142
145
|
`;
|
|
143
146
|
}
|
|
144
147
|
|
|
145
|
-
// ============================================================================
|
|
146
|
-
// 状态检测
|
|
147
|
-
// ============================================================================
|
|
148
|
-
|
|
149
|
-
function detectState() {
|
|
150
|
-
try {
|
|
151
|
-
execSync('wsl echo ok', { stdio: 'pipe', timeout: 15000 });
|
|
152
|
-
return 'ready';
|
|
153
|
-
} catch {}
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
execSync('wsl --status', { stdio: 'pipe', timeout: 5000 });
|
|
157
|
-
return 'needs-setup';
|
|
158
|
-
} catch {}
|
|
159
|
-
|
|
160
|
-
return 'needs-features';
|
|
161
|
-
}
|
|
162
|
-
|
|
163
148
|
// ============================================================================
|
|
164
149
|
// Phase 1: 启用功能 + 安装 WSL
|
|
165
150
|
// ============================================================================
|
|
166
151
|
|
|
167
152
|
function runPhase1() {
|
|
168
153
|
console.log('');
|
|
169
|
-
console.log('[当前进度] WSL 功能 ' + C.
|
|
170
|
-
console.log(' WSL 组件 ' + C.
|
|
154
|
+
console.log('[当前进度] WSL 功能 ' + C.y + '[安装/覆盖]' + C.nc);
|
|
155
|
+
console.log(' WSL 组件 ' + C.y + '[安装/重置]' + C.nc);
|
|
171
156
|
console.log(' Linux ' + C.r + '[未安装]' + C.nc);
|
|
172
157
|
console.log('');
|
|
173
|
-
console.log('阶段 1/2:
|
|
158
|
+
console.log('阶段 1/2: 强制执行底层系统组件配置');
|
|
174
159
|
console.log(' 完成后需要 ' + C.y + '重启电脑' + C.nc);
|
|
175
160
|
console.log('');
|
|
176
161
|
console.log('[' + C.y + '注意' + C.nc + '] 请在 UAC 弹窗中点击【是】');
|
|
@@ -194,15 +179,16 @@ Write-Host ' MyClaw WSL2 安装 - 阶段 1/2'
|
|
|
194
179
|
Write-Host '========================================'
|
|
195
180
|
Write-Host ''
|
|
196
181
|
|
|
197
|
-
$dir = "$env:LOCALAPPDATA
|
|
182
|
+
$dir = "$env:LOCALAPPDATA\\myclaw"
|
|
198
183
|
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
|
184
|
+
$msi = "$dir\\wsl_full.msi"
|
|
199
185
|
|
|
200
186
|
Write-Host '[1/4] 启用 Windows Subsystem for Linux...'
|
|
201
187
|
try {
|
|
202
188
|
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart | Out-Null
|
|
203
189
|
Write-Host ' [OK]'
|
|
204
190
|
} catch {
|
|
205
|
-
Write-Host '
|
|
191
|
+
Write-Host ' [异常] 启用失败,继续尝试...'
|
|
206
192
|
}
|
|
207
193
|
Write-Host ''
|
|
208
194
|
|
|
@@ -211,34 +197,37 @@ try {
|
|
|
211
197
|
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart | Out-Null
|
|
212
198
|
Write-Host ' [OK]'
|
|
213
199
|
} catch {
|
|
214
|
-
Write-Host '
|
|
200
|
+
Write-Host ' [异常] 启用失败,继续尝试...'
|
|
215
201
|
}
|
|
216
202
|
Write-Host ''
|
|
217
203
|
|
|
218
204
|
Write-Host '[3/4] 获取 WSL 安装包...'
|
|
219
|
-
|
|
220
|
-
$
|
|
221
|
-
try {
|
|
222
|
-
$ver = wsl --version 2>&1 | Out-String
|
|
223
|
-
if ($ver -match 'WSL') { $wslInstalled = $true }
|
|
224
|
-
} catch {}
|
|
205
|
+
# 移除文件以保证不做本地缓存
|
|
206
|
+
if (Test-Path $msi) { Remove-Item $msi -Force }
|
|
225
207
|
|
|
226
|
-
if ($wslInstalled) {
|
|
227
|
-
Write-Host ' WSL 已安装,跳过'
|
|
228
|
-
Write-Host ' [OK]'
|
|
229
|
-
} else {
|
|
230
208
|
${askMsi}
|
|
231
209
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
210
|
+
Write-Host ''
|
|
211
|
+
Write-Host '[4/4] 强制覆盖安装 WSL...'
|
|
212
|
+
try {
|
|
213
|
+
Start-Process msiexec.exe -ArgumentList @('/i', $msi, '/quiet', '/norestart') -Wait -NoNewWindow
|
|
214
|
+
if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 3010) {
|
|
215
|
+
Write-Host " [警告] 安装退出代码异常 (Code: $LASTEXITCODE)"
|
|
216
|
+
} else {
|
|
236
217
|
Write-Host ' [OK]'
|
|
237
|
-
} catch {
|
|
238
|
-
Write-Host ' [失败] 安装出错'
|
|
239
218
|
}
|
|
219
|
+
} catch {
|
|
220
|
+
Write-Host ' [失败] 安装出错'
|
|
221
|
+
throw 'MSI 安装失败'
|
|
240
222
|
}
|
|
241
223
|
|
|
224
|
+
# 装完立刻删除缓存
|
|
225
|
+
if (Test-Path $msi) { Remove-Item $msi -Force }
|
|
226
|
+
|
|
227
|
+
# 提权环境下,通过指定绝对路径来将标记写给原来普通用户
|
|
228
|
+
$markerPath = "${STATE_FILE.replace(/\\/g, '\\\\')}"
|
|
229
|
+
Out-File -FilePath "$markerPath" -InputObject "phase1-done" -Encoding utf8 -Force
|
|
230
|
+
|
|
242
231
|
Write-Host ''
|
|
243
232
|
Write-Host '========================================'
|
|
244
233
|
Write-Host ' 阶段 1/2 完成!'
|
|
@@ -275,8 +264,8 @@ Write-Host '========================================'
|
|
|
275
264
|
|
|
276
265
|
function runPhase2() {
|
|
277
266
|
console.log('');
|
|
278
|
-
console.log('[当前进度] WSL 功能 ' + C.g + '[
|
|
279
|
-
console.log(' WSL 组件 ' + C.g + '[
|
|
267
|
+
console.log('[当前进度] WSL 功能 ' + C.g + '[已就绪]' + C.nc);
|
|
268
|
+
console.log(' WSL 组件 ' + C.g + '[已就绪]' + C.nc);
|
|
280
269
|
console.log(' Linux ' + C.y + '[待安装]' + C.nc);
|
|
281
270
|
console.log('');
|
|
282
271
|
console.log('即将导入预制 Linux 环境...');
|
|
@@ -302,8 +291,9 @@ Write-Host ' 导入预制 Linux 环境'
|
|
|
302
291
|
Write-Host '========================================'
|
|
303
292
|
Write-Host ''
|
|
304
293
|
|
|
305
|
-
$dir = "$env:LOCALAPPDATA
|
|
294
|
+
$dir = "$env:LOCALAPPDATA\\myclaw"
|
|
306
295
|
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
|
296
|
+
$tarPath = "$dir\\openclaw-rootfs.tar"
|
|
307
297
|
|
|
308
298
|
Write-Host '[1/3] 设置 WSL2 为默认版本...'
|
|
309
299
|
try { wsl --set-default-version 2 2>$null } catch {}
|
|
@@ -311,16 +301,17 @@ Write-Host ' [OK]'
|
|
|
311
301
|
Write-Host ''
|
|
312
302
|
|
|
313
303
|
Write-Host '[2/3] 获取预制 Linux 环境...'
|
|
314
|
-
$tarPath = "$dir\\\\openclaw-rootfs.tar"
|
|
315
304
|
$distroName = 'OpenClaw'
|
|
316
|
-
$installDir = "$dir
|
|
305
|
+
$installDir = "$dir\\OpenClaw"
|
|
317
306
|
$installed = $false
|
|
318
307
|
|
|
308
|
+
# 移除旧的缓存
|
|
309
|
+
if (Test-Path $tarPath) { Remove-Item $tarPath -Force }
|
|
310
|
+
|
|
319
311
|
${askTar}
|
|
320
312
|
|
|
321
313
|
Write-Host ' 正在导入 (可能需要几分钟)...'
|
|
322
314
|
|
|
323
|
-
# 先尝试卸载旧的同名发行版(不存在则忽略)
|
|
324
315
|
Write-Host ' 清理旧版本...'
|
|
325
316
|
try { wsl --unregister $distroName 2>$null } catch {}
|
|
326
317
|
Write-Host ' [OK]'
|
|
@@ -339,6 +330,9 @@ try {
|
|
|
339
330
|
}
|
|
340
331
|
Write-Host ''
|
|
341
332
|
|
|
333
|
+
# 导入完毕清理 TAR 缓存
|
|
334
|
+
if (Test-Path $tarPath) { Remove-Item $tarPath -Force }
|
|
335
|
+
|
|
342
336
|
Write-Host '[3/3] 验证安装结果...'
|
|
343
337
|
try {
|
|
344
338
|
$result = wsl -l -v 2>&1 | Out-String
|
|
@@ -354,17 +348,18 @@ if ($installed) {
|
|
|
354
348
|
Write-Host '========================================'
|
|
355
349
|
Write-Host ''
|
|
356
350
|
|
|
357
|
-
|
|
351
|
+
$markerPath = "${STATE_FILE.replace(/\\/g, '\\\\')}"
|
|
352
|
+
Out-File -FilePath "$markerPath" -InputObject "phase2-done" -Encoding utf8 -Force
|
|
353
|
+
|
|
358
354
|
Write-Host '[镜像信息]'
|
|
359
355
|
try {
|
|
360
356
|
$buildInfo = wsl -d $distroName -- cat /etc/openclaw-build-info 2>&1
|
|
361
357
|
Write-Host $buildInfo
|
|
362
358
|
} catch {
|
|
363
|
-
Write-Host ' (
|
|
359
|
+
Write-Host ' (无构建信息)'
|
|
364
360
|
}
|
|
365
361
|
Write-Host ''
|
|
366
362
|
|
|
367
|
-
# 统一启动流程:update + patch + gateway + open
|
|
368
363
|
wsl -d $distroName -- myclaw launch
|
|
369
364
|
} else {
|
|
370
365
|
Write-Host ' [失败] 请检查后重试: myclaw wsl2'
|
|
@@ -402,14 +397,22 @@ function run() {
|
|
|
402
397
|
process.exit(0);
|
|
403
398
|
}
|
|
404
399
|
|
|
400
|
+
// 隐藏的高级玩家后门,强制跳过阶段
|
|
401
|
+
if (process.argv.includes('--force-phase1')) {
|
|
402
|
+
setState('needs-features');
|
|
403
|
+
}
|
|
404
|
+
if (process.argv.includes('--force-phase2')) {
|
|
405
|
+
setState('phase1-done');
|
|
406
|
+
}
|
|
407
|
+
|
|
405
408
|
const bar = '========================================';
|
|
406
|
-
const state =
|
|
409
|
+
const state = getState();
|
|
407
410
|
|
|
408
411
|
console.log('');
|
|
409
412
|
console.log('[' + C.b + 'MyClaw' + C.nc + '] WSL2 一键安装向导');
|
|
410
413
|
console.log(bar);
|
|
411
414
|
|
|
412
|
-
if (state === '
|
|
415
|
+
if (state === 'phase2-done') {
|
|
413
416
|
console.log('');
|
|
414
417
|
console.log('[' + C.g + 'OK' + C.nc + '] WSL2 已完全安装就绪!');
|
|
415
418
|
console.log('');
|
|
@@ -423,15 +426,15 @@ function run() {
|
|
|
423
426
|
console.log('输入 ' + C.y + 'wsl' + C.nc + ' 即可进入 Linux 环境。');
|
|
424
427
|
console.log('');
|
|
425
428
|
|
|
426
|
-
// 询问是否重装
|
|
427
429
|
const readline = require('readline');
|
|
428
430
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
429
|
-
rl.question('
|
|
431
|
+
rl.question('是否要重置状态并完全重新走安装流程? (y/N): ', (answer) => {
|
|
430
432
|
rl.close();
|
|
431
433
|
if (answer.trim().toLowerCase() === 'y') {
|
|
432
434
|
console.log('');
|
|
433
|
-
console.log('[' + C.y + '
|
|
434
|
-
|
|
435
|
+
console.log('[' + C.y + '重置模式' + C.nc + '] 将删除标记文件并从 Phase 1 重新开始...');
|
|
436
|
+
setState('needs-features');
|
|
437
|
+
runPhase1();
|
|
435
438
|
} else {
|
|
436
439
|
console.log('');
|
|
437
440
|
console.log('已取消。');
|
|
@@ -440,7 +443,7 @@ function run() {
|
|
|
440
443
|
return;
|
|
441
444
|
}
|
|
442
445
|
|
|
443
|
-
if (state === '
|
|
446
|
+
if (state === 'phase1-done') {
|
|
444
447
|
runPhase2();
|
|
445
448
|
} else {
|
|
446
449
|
runPhase1();
|