@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.
@@ -3,12 +3,12 @@
3
3
  * MyClaw TTS — 讯飞语音合成
4
4
  * ============================================================================
5
5
  *
6
- * 功能:拦截 chat-tts-btn 按钮,点击后使用讯飞 TTS 播放消息
6
+ * 功能:在每条消息的 .chat-group-footer 左侧注入播放按钮,用文字显示状态
7
7
  *
8
8
  * 状态:
9
- * idle 默认状态,显示小嘴巴 emoji
10
- * generating 合成中,显示加载动画
11
- * playing 播放中,显示喇叭 emoji,点击可停止
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 ICONS = {
50
- idle: '👁', // 👁 小嘴巴
51
- generating: '🔄', // 🔄 加载中
52
- playing: '🔊', // 🔊 喇叭
47
+ // ═══ 状态文字 ═══
48
+ var STATE_TEXT = {
49
+ idle: '播放',
50
+ generating: '生成中',
51
+ playing: '播放中',
53
52
  };
54
53
 
55
- // ═══ 状态更新 ═══
56
- function setPlayerState(btn, state) {
57
- if (!btn) return;
58
- playerState = state;
54
+ // ═══ 提取消息文字 ═══
55
+ function extractMessageText(btn) {
56
+ var group = btn.closest('.chat-group-messages');
57
+ if (!group) return '';
59
58
 
60
- // 恢复原生 SVG
61
- var svg = btn.querySelector('svg');
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
- // 移除所有状态 class
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
- function updateBtnIcon(btn, state) {
81
- if (!btn) return;
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
- // 移除旧的 emoji 容器
84
- var oldEmoji = btn.querySelector('.myclaw-tts-icon');
85
- if (oldEmoji) oldEmoji.remove();
103
+ var state = playerState;
104
+ var text = extractMessageText(btn);
86
105
 
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;';
106
+ if (!text) {
107
+ console.warn('[myclaw-tts] 未找到消息文字');
108
+ return;
109
+ }
92
110
 
93
- // 插入到按钮最前面
94
- btn.insertBefore(emoji, btn.firstChild);
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 (!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 时处理
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
- setPlayerState(currentBtn, 'idle');
133
- updateBtnIcon(currentBtn, 'idle');
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--generating {',
166
- ' animation: myclaw-tts-spin 1s linear infinite;',
167
- ' opacity: 0.7;',
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
- '@keyframes myclaw-tts-spin {',
170
- ' from { transform: rotate(0deg); }',
171
- ' to { transform: rotate(360deg); }',
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
- ' animation: myclaw-tts-pulse 0.8s ease-in-out infinite;',
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-pulse {',
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
- hookTtsButton();
263
+ observeFooters();
264
+ console.log('[myclaw-tts] ✅ 初始化完成');
263
265
  }
264
266
 
265
267
  if (document.readyState === 'loading') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.0.123",
3
+ "version": "1.0.125",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_doc": "MyClaw 注入清单 (auto-generated)。strategy: auto | on | off | delete",
3
- "_generated": "2026-04-02T15:59:10.499Z",
3
+ "_generated": "2026-04-03T03:31:20.673Z",
4
4
  "agents": [
5
5
  {
6
6
  "id": "danci",
package/wsl2.js CHANGED
@@ -5,20 +5,18 @@
5
5
  * MyClaw WSL2 一键安装器
6
6
  * ============================================================================
7
7
  *
8
- * 使用方式: myclaw wsl2
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 launchElevatedPS(script) {
43
- const fs = require('fs');
44
- const path = require('path');
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 — PowerShell 需要 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.r + '[未启用]' + C.nc);
170
- console.log(' WSL 组件 ' + C.r + '[未安装]' + C.nc);
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: 启用底层功能 + 安装 WSL');
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\\\\myclaw"
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
- $msi = "$dir\\\\wsl_full.msi"
220
- $wslInstalled = $false
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
- Write-Host ''
233
- Write-Host '[4/4] 安装 WSL...'
234
- try {
235
- Start-Process msiexec.exe -ArgumentList @('/i', $msi, '/quiet', '/norestart') -Wait -NoNewWindow
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 + '[OK]' + C.nc);
279
- console.log(' WSL 组件 ' + C.g + '[OK]' + C.nc);
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\\\\myclaw"
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\\\\OpenClaw"
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 = detectState();
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 === 'ready') {
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('是否要重新安装 Linux 环境? (y/N): ', (answer) => {
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 + '重装模式' + C.nc + '] 将重新导入 Linux 环境...');
434
- runPhase2();
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 === 'needs-setup') {
446
+ if (state === 'phase1-done') {
444
447
  runPhase2();
445
448
  } else {
446
449
  runPhase1();