@aiyiran/myclaw 1.0.122 → 1.0.124
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 +76 -79
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) {
|
|
@@ -142,35 +139,17 @@ if ($resolved) {
|
|
|
142
139
|
`;
|
|
143
140
|
}
|
|
144
141
|
|
|
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
142
|
// ============================================================================
|
|
164
143
|
// Phase 1: 启用功能 + 安装 WSL
|
|
165
144
|
// ============================================================================
|
|
166
145
|
|
|
167
146
|
function runPhase1() {
|
|
168
147
|
console.log('');
|
|
169
|
-
console.log('[当前进度] WSL 功能 ' + C.
|
|
170
|
-
console.log(' WSL 组件 ' + C.
|
|
148
|
+
console.log('[当前进度] WSL 功能 ' + C.y + '[安装/覆盖]' + C.nc);
|
|
149
|
+
console.log(' WSL 组件 ' + C.y + '[安装/重置]' + C.nc);
|
|
171
150
|
console.log(' Linux ' + C.r + '[未安装]' + C.nc);
|
|
172
151
|
console.log('');
|
|
173
|
-
console.log('阶段 1/2:
|
|
152
|
+
console.log('阶段 1/2: 强制执行底层系统组件配置');
|
|
174
153
|
console.log(' 完成后需要 ' + C.y + '重启电脑' + C.nc);
|
|
175
154
|
console.log('');
|
|
176
155
|
console.log('[' + C.y + '注意' + C.nc + '] 请在 UAC 弹窗中点击【是】');
|
|
@@ -194,15 +173,16 @@ Write-Host ' MyClaw WSL2 安装 - 阶段 1/2'
|
|
|
194
173
|
Write-Host '========================================'
|
|
195
174
|
Write-Host ''
|
|
196
175
|
|
|
197
|
-
$dir = "$env:LOCALAPPDATA
|
|
176
|
+
$dir = "$env:LOCALAPPDATA\\myclaw"
|
|
198
177
|
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
|
178
|
+
$msi = "$dir\\wsl_full.msi"
|
|
199
179
|
|
|
200
180
|
Write-Host '[1/4] 启用 Windows Subsystem for Linux...'
|
|
201
181
|
try {
|
|
202
182
|
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart | Out-Null
|
|
203
183
|
Write-Host ' [OK]'
|
|
204
184
|
} catch {
|
|
205
|
-
Write-Host '
|
|
185
|
+
Write-Host ' [异常] 启用失败,继续尝试...'
|
|
206
186
|
}
|
|
207
187
|
Write-Host ''
|
|
208
188
|
|
|
@@ -211,34 +191,37 @@ try {
|
|
|
211
191
|
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart | Out-Null
|
|
212
192
|
Write-Host ' [OK]'
|
|
213
193
|
} catch {
|
|
214
|
-
Write-Host '
|
|
194
|
+
Write-Host ' [异常] 启用失败,继续尝试...'
|
|
215
195
|
}
|
|
216
196
|
Write-Host ''
|
|
217
197
|
|
|
218
198
|
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 {}
|
|
199
|
+
# 移除文件以保证不做本地缓存
|
|
200
|
+
if (Test-Path $msi) { Remove-Item $msi -Force }
|
|
225
201
|
|
|
226
|
-
if ($wslInstalled) {
|
|
227
|
-
Write-Host ' WSL 已安装,跳过'
|
|
228
|
-
Write-Host ' [OK]'
|
|
229
|
-
} else {
|
|
230
202
|
${askMsi}
|
|
231
203
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
204
|
+
Write-Host ''
|
|
205
|
+
Write-Host '[4/4] 强制覆盖安装 WSL...'
|
|
206
|
+
try {
|
|
207
|
+
Start-Process msiexec.exe -ArgumentList @('/i', $msi, '/quiet', '/norestart') -Wait -NoNewWindow
|
|
208
|
+
if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 3010) {
|
|
209
|
+
Write-Host " [警告] 安装退出代码异常 (Code: $LASTEXITCODE)"
|
|
210
|
+
} else {
|
|
236
211
|
Write-Host ' [OK]'
|
|
237
|
-
} catch {
|
|
238
|
-
Write-Host ' [失败] 安装出错'
|
|
239
212
|
}
|
|
213
|
+
} catch {
|
|
214
|
+
Write-Host ' [失败] 安装出错'
|
|
215
|
+
throw 'MSI 安装失败'
|
|
240
216
|
}
|
|
241
217
|
|
|
218
|
+
# 装完立刻删除缓存
|
|
219
|
+
if (Test-Path $msi) { Remove-Item $msi -Force }
|
|
220
|
+
|
|
221
|
+
# 提权环境下,通过指定绝对路径来将标记写给原来普通用户
|
|
222
|
+
$markerPath = "${STATE_FILE.replace(/\\/g, '\\\\')}"
|
|
223
|
+
Out-File -FilePath "$markerPath" -InputObject "phase1-done" -Encoding utf8 -Force
|
|
224
|
+
|
|
242
225
|
Write-Host ''
|
|
243
226
|
Write-Host '========================================'
|
|
244
227
|
Write-Host ' 阶段 1/2 完成!'
|
|
@@ -275,8 +258,8 @@ Write-Host '========================================'
|
|
|
275
258
|
|
|
276
259
|
function runPhase2() {
|
|
277
260
|
console.log('');
|
|
278
|
-
console.log('[当前进度] WSL 功能 ' + C.g + '[
|
|
279
|
-
console.log(' WSL 组件 ' + C.g + '[
|
|
261
|
+
console.log('[当前进度] WSL 功能 ' + C.g + '[已就绪]' + C.nc);
|
|
262
|
+
console.log(' WSL 组件 ' + C.g + '[已就绪]' + C.nc);
|
|
280
263
|
console.log(' Linux ' + C.y + '[待安装]' + C.nc);
|
|
281
264
|
console.log('');
|
|
282
265
|
console.log('即将导入预制 Linux 环境...');
|
|
@@ -302,8 +285,9 @@ Write-Host ' 导入预制 Linux 环境'
|
|
|
302
285
|
Write-Host '========================================'
|
|
303
286
|
Write-Host ''
|
|
304
287
|
|
|
305
|
-
$dir = "$env:LOCALAPPDATA
|
|
288
|
+
$dir = "$env:LOCALAPPDATA\\myclaw"
|
|
306
289
|
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
|
290
|
+
$tarPath = "$dir\\openclaw-rootfs.tar"
|
|
307
291
|
|
|
308
292
|
Write-Host '[1/3] 设置 WSL2 为默认版本...'
|
|
309
293
|
try { wsl --set-default-version 2 2>$null } catch {}
|
|
@@ -311,16 +295,17 @@ Write-Host ' [OK]'
|
|
|
311
295
|
Write-Host ''
|
|
312
296
|
|
|
313
297
|
Write-Host '[2/3] 获取预制 Linux 环境...'
|
|
314
|
-
$tarPath = "$dir\\\\openclaw-rootfs.tar"
|
|
315
298
|
$distroName = 'OpenClaw'
|
|
316
|
-
$installDir = "$dir
|
|
299
|
+
$installDir = "$dir\\OpenClaw"
|
|
317
300
|
$installed = $false
|
|
318
301
|
|
|
302
|
+
# 移除旧的缓存
|
|
303
|
+
if (Test-Path $tarPath) { Remove-Item $tarPath -Force }
|
|
304
|
+
|
|
319
305
|
${askTar}
|
|
320
306
|
|
|
321
307
|
Write-Host ' 正在导入 (可能需要几分钟)...'
|
|
322
308
|
|
|
323
|
-
# 先尝试卸载旧的同名发行版(不存在则忽略)
|
|
324
309
|
Write-Host ' 清理旧版本...'
|
|
325
310
|
try { wsl --unregister $distroName 2>$null } catch {}
|
|
326
311
|
Write-Host ' [OK]'
|
|
@@ -339,6 +324,9 @@ try {
|
|
|
339
324
|
}
|
|
340
325
|
Write-Host ''
|
|
341
326
|
|
|
327
|
+
# 导入完毕清理 TAR 缓存
|
|
328
|
+
if (Test-Path $tarPath) { Remove-Item $tarPath -Force }
|
|
329
|
+
|
|
342
330
|
Write-Host '[3/3] 验证安装结果...'
|
|
343
331
|
try {
|
|
344
332
|
$result = wsl -l -v 2>&1 | Out-String
|
|
@@ -354,17 +342,18 @@ if ($installed) {
|
|
|
354
342
|
Write-Host '========================================'
|
|
355
343
|
Write-Host ''
|
|
356
344
|
|
|
357
|
-
|
|
345
|
+
$markerPath = "${STATE_FILE.replace(/\\/g, '\\\\')}"
|
|
346
|
+
Out-File -FilePath "$markerPath" -InputObject "phase2-done" -Encoding utf8 -Force
|
|
347
|
+
|
|
358
348
|
Write-Host '[镜像信息]'
|
|
359
349
|
try {
|
|
360
350
|
$buildInfo = wsl -d $distroName -- cat /etc/openclaw-build-info 2>&1
|
|
361
351
|
Write-Host $buildInfo
|
|
362
352
|
} catch {
|
|
363
|
-
Write-Host ' (
|
|
353
|
+
Write-Host ' (无构建信息)'
|
|
364
354
|
}
|
|
365
355
|
Write-Host ''
|
|
366
356
|
|
|
367
|
-
# 统一启动流程:update + patch + gateway + open
|
|
368
357
|
wsl -d $distroName -- myclaw launch
|
|
369
358
|
} else {
|
|
370
359
|
Write-Host ' [失败] 请检查后重试: myclaw wsl2'
|
|
@@ -402,14 +391,22 @@ function run() {
|
|
|
402
391
|
process.exit(0);
|
|
403
392
|
}
|
|
404
393
|
|
|
394
|
+
// 隐藏的高级玩家后门,强制跳过阶段
|
|
395
|
+
if (process.argv.includes('--force-phase1')) {
|
|
396
|
+
setState('needs-features');
|
|
397
|
+
}
|
|
398
|
+
if (process.argv.includes('--force-phase2')) {
|
|
399
|
+
setState('phase1-done');
|
|
400
|
+
}
|
|
401
|
+
|
|
405
402
|
const bar = '========================================';
|
|
406
|
-
const state =
|
|
403
|
+
const state = getState();
|
|
407
404
|
|
|
408
405
|
console.log('');
|
|
409
406
|
console.log('[' + C.b + 'MyClaw' + C.nc + '] WSL2 一键安装向导');
|
|
410
407
|
console.log(bar);
|
|
411
408
|
|
|
412
|
-
if (state === '
|
|
409
|
+
if (state === 'phase2-done') {
|
|
413
410
|
console.log('');
|
|
414
411
|
console.log('[' + C.g + 'OK' + C.nc + '] WSL2 已完全安装就绪!');
|
|
415
412
|
console.log('');
|
|
@@ -423,15 +420,15 @@ function run() {
|
|
|
423
420
|
console.log('输入 ' + C.y + 'wsl' + C.nc + ' 即可进入 Linux 环境。');
|
|
424
421
|
console.log('');
|
|
425
422
|
|
|
426
|
-
// 询问是否重装
|
|
427
423
|
const readline = require('readline');
|
|
428
424
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
429
|
-
rl.question('
|
|
425
|
+
rl.question('是否要重置状态并完全重新走安装流程? (y/N): ', (answer) => {
|
|
430
426
|
rl.close();
|
|
431
427
|
if (answer.trim().toLowerCase() === 'y') {
|
|
432
428
|
console.log('');
|
|
433
|
-
console.log('[' + C.y + '
|
|
434
|
-
|
|
429
|
+
console.log('[' + C.y + '重置模式' + C.nc + '] 将删除标记文件并从 Phase 1 重新开始...');
|
|
430
|
+
setState('needs-features');
|
|
431
|
+
runPhase1();
|
|
435
432
|
} else {
|
|
436
433
|
console.log('');
|
|
437
434
|
console.log('已取消。');
|
|
@@ -440,7 +437,7 @@ function run() {
|
|
|
440
437
|
return;
|
|
441
438
|
}
|
|
442
439
|
|
|
443
|
-
if (state === '
|
|
440
|
+
if (state === 'phase1-done') {
|
|
444
441
|
runPhase2();
|
|
445
442
|
} else {
|
|
446
443
|
runPhase1();
|