@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.
@@ -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.122",
3
+ "version": "1.0.124",
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:57:57.414Z",
3
+ "_generated": "2026-04-03T03:21:17.226Z",
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) {
@@ -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.r + '[未启用]' + C.nc);
170
- console.log(' WSL 组件 ' + C.r + '[未安装]' + C.nc);
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: 启用底层功能 + 安装 WSL');
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\\\\myclaw"
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
- $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 {}
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
- Write-Host ''
233
- Write-Host '[4/4] 安装 WSL...'
234
- try {
235
- Start-Process msiexec.exe -ArgumentList @('/i', $msi, '/quiet', '/norestart') -Wait -NoNewWindow
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 + '[OK]' + C.nc);
279
- console.log(' WSL 组件 ' + C.g + '[OK]' + C.nc);
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\\\\myclaw"
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\\\\OpenClaw"
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 = detectState();
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 === 'ready') {
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('是否要重新安装 Linux 环境? (y/N): ', (answer) => {
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 + '重装模式' + C.nc + '] 将重新导入 Linux 环境...');
434
- runPhase2();
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 === 'needs-setup') {
440
+ if (state === 'phase1-done') {
444
441
  runPhase2();
445
442
  } else {
446
443
  runPhase1();