cicy-desktop 2.1.298 → 2.1.300

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cicy-desktop",
3
- "version": "2.1.298",
3
+ "version": "2.1.300",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -147,8 +147,8 @@ function register({ sidecarLogPath } = {}) {
147
147
  let _autoBootstrapPaused = null;
148
148
  let _autoBootstrapRetryAt = 0; // 硬失败后的自动重试时间点(退避,不是永久暂停:用户不会自己修)
149
149
  // 退避:子进程被拦(安全软件开机后一段时间会自己放行)2 分钟一试;其余 15 分钟一试。
150
- const AUTO_RETRY_MS = { spawn_blocked: 2 * 60 * 1000, default: 15 * 60 * 1000 };
151
- const HARD_BOOTSTRAP_REASONS = new Set(["spawn_blocked", "virtualization_disabled", "wsl_enable_failed", "wsl_kernel_missing", "wsl_reboot_required"]);
150
+ const AUTO_RETRY_MS = { spawn_blocked: 2 * 60 * 1000, windows_component_store_broken: 6 * 60 * 60 * 1000, virtualization_disabled: 60 * 60 * 1000, default: 15 * 60 * 1000 };
151
+ const HARD_BOOTSTRAP_REASONS = new Set(["spawn_blocked", "virtualization_disabled", "wsl_enable_failed", "windows_component_store_broken", "wsl_kernel_missing", "wsl_reboot_required"]);
152
152
  function recordBootstrapResult(result, err) {
153
153
  if (err) {
154
154
  const code = err && err.code;
@@ -169,6 +169,7 @@ function register({ sidecarLogPath } = {}) {
169
169
  // WSL 功能启用后必须重启 Windows 才能继续。不等人:自动安排 90 秒后重启(桌面上会有
170
170
  // 系统倒计时提示),UI 可「取消」或「立即重启」。每次开机只安排一次。
171
171
  let _rebootScheduled = false;
172
+ let _relayMisses = 0, _relayResetDone = false; // WSL localhost 转发自愈(见 daemon 循环)
172
173
  // 防重启循环:WSL 始终起不来时不能每次开机都重启。计数存 db/wsl-reboots.json,最多自动重启 2 次
173
174
  // (成功后 :8008 起来时清零)。
174
175
  const REBOOT_COUNT_FILE = path.join(os.homedir(), "cicy-ai", "db", "wsl-reboots.json");
@@ -227,6 +228,25 @@ function register({ sidecarLogPath } = {}) {
227
228
  // bootstrap 幂等且可续跑;首次启用 WSL 若要求重启 Windows,下次登录 Desktop
228
229
  // 会再次进入这里并从已完成的步骤继续。unknown 表示 WSL 正卡住/未响应,
229
230
  // 此时不并发安装,留给下一轮检测或显式「修复 WSL」。
231
+ // 容器在里面健康、Windows 侧却连不通 = WSL localhost 转发坏了(实测 wsl --shutdown 后恢复)。
232
+ // 连续 2 轮(≈2 分钟)如此 → 每次开机最多自动重置一次 WSL。数据在 volume 里,容器由
233
+ // unless-stopped 策略自动拉起;不算「销毁」——对用户来说它本来就已经不可用了。
234
+ if (!s.running && !s.unknown && s.installed && appDocker.insideHealthy && appDocker.wslShutdown) {
235
+ let inside = false;
236
+ try { inside = await appDocker.insideHealthy(APP_CONTAINER, APP_PORT); } catch {}
237
+ if (inside) {
238
+ _relayMisses += 1;
239
+ if (_relayMisses >= 2 && !_relayResetDone) {
240
+ _relayResetDone = true;
241
+ auditDestructiveIpc(log, "docker:self-heal-localhost-relay", null, { container: APP_CONTAINER, misses: _relayMisses });
242
+ log.warn("[docker-daemon] container healthy inside WSL but 127.0.0.1:8008 unreachable from Windows → wsl --shutdown once to rebuild the localhost relay");
243
+ try { await appDocker.wslShutdown(); } catch (e) { log.warn(`[docker-daemon] wsl --shutdown failed: ${e.message}`); }
244
+ await refreshDockerStatus();
245
+ return;
246
+ }
247
+ if (_relayMisses < 2) { log.warn(`[docker-daemon] :${APP_PORT} unreachable from Windows but healthy inside (${_relayMisses}/2)`); return; }
248
+ } else { _relayMisses = 0; }
249
+ } else if (s.running) { _relayMisses = 0; }
230
250
  if (!s.running && !s.unknown && _autoBootstrapPaused && Date.now() < _autoBootstrapRetryAt) {
231
251
  // 硬失败后退避(见 recordBootstrapResult);卡片显示 lastError,到点自动再试,用户点「重试」也行。
232
252
  } else if (!s.running && !s.unknown) {
@@ -738,7 +738,10 @@ function elevatedWslSetup({ emit, need = {} } = {}) {
738
738
  'Say "start admin=$(([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(\'Administrators\'))"',
739
739
  '$feats = @("Microsoft-Windows-Subsystem-Linux","VirtualMachinePlatform")',
740
740
  '$bad = @(); foreach ($f in $feats) { if ((St $f) -ne 1) { $c = En $f; if ($c -ne 0 -and $c -ne 3010) { $bad += $f } } }',
741
- 'if ($bad.Count -gt 0) {',
741
+ `$REPAIRED = "${path.join(dir, "wsl-repair-attempted.txt").replace(/"/g, '""')}"`,
742
+ 'if ($bad.Count -gt 0 -and (Test-Path $REPAIRED)) { Say "repair already attempted before and the feature still cannot be enabled — skipping the 30-min repair; this Windows image is missing the feature payload (reinstall/repair Windows needed)" }',
743
+ 'if ($bad.Count -gt 0 -and -not (Test-Path $REPAIRED)) {',
744
+ ' Set-Content -Path $REPAIRED -Value (Get-Date -Format s)',
742
745
  ' Say "repair: DISM /RestoreHealth (component store refused: $($bad -join \',\')) — this can take 10-30 min"',
743
746
  ' & $DISM /online /cleanup-image /restorehealth /norestart | Out-Null; Say ("restorehealth exit=" + $LASTEXITCODE)',
744
747
  ' & $SFC /scannow | Out-Null; Say ("sfc exit=" + $LASTEXITCODE)',
@@ -768,7 +771,7 @@ function elevatedWslSetup({ emit, need = {} } = {}) {
768
771
  try { lines = fs.readFileSync(resultFile, "utf8").split(/\r?\n/).filter(Boolean); } catch {}
769
772
  for (const l of lines.slice(seen)) {
770
773
  emit && emit({ phase: "install-docker", status: "running", message: `提权脚本:${l.replace(/^\[[^\]]*\] /, "")}` });
771
- if (/exit=(?!0\b|3010\b)\d+/.test(l)) detail = l;
774
+ if (/exit=(?!0\b|3010\b)\d+|repair already attempted/.test(l)) detail = l;
772
775
  }
773
776
  seen = lines.length;
774
777
  if (lines.some((l) => /DONE/.test(l))) break;
@@ -869,10 +872,13 @@ async function ensureWsl({ emit } = {}) {
869
872
  const r = await elevatedWslSetup({ emit, need: { subsystem: !subsystemEnabled, vmPlatform: !vmPlatformEnabled } });
870
873
  const a = r.subsystem, b = r.vmPlatform;
871
874
  if (!a || !b) {
872
- const message = `WSL 功能未能全部启用(Linux 子系统=${a ? "已启用" : "失败"},虚拟机平台=${b ? "已启用" : "失败"}${r.detail ? `;${r.detail}` : ";通常是 UAC 被取消或当前账号无管理员权限"})——会自动重试;也可以点「重试」。`;
875
+ const storeBroken = /exit=(14107|3017)|repair already attempted/.test(r.detail || "");
876
+ const message = storeBroken
877
+ ? `Windows 系统组件缺失或损坏,无法启用「虚拟机平台」(DISM 修复后仍失败,错误 14107)。这台 Windows 需要用安装镜像修复(DISM /Source)或重装系统后才能使用 WSL2/Docker。`
878
+ : `WSL 功能未能全部启用(Linux 子系统=${a ? "已启用" : "失败"},虚拟机平台=${b ? "已启用" : "失败"}${r.detail ? `;${r.detail}` : ";通常是 UAC 被取消或当前账号无管理员权限"})——会自动重试;也可以点「重试」。`;
873
879
  log.error(`[bootstrap] ✗ ensure-wsl reason=wsl_enable_failed subsystem=${a} vmPlatform=${b}`);
874
880
  emit && emit({ phase: "done", status: "error", message });
875
- return { ok: false, needsReboot: false, failed: true, reason: "wsl_enable_failed", message };
881
+ return { ok: false, needsReboot: false, failed: true, reason: storeBroken ? "windows_component_store_broken" : "wsl_enable_failed", message };
876
882
  }
877
883
  // Best-effort: also pull the WSL2 kernel/plumbing when the executable itself
878
884
  // is missing. Feature-only repairs must stop here and wait for reboot.
@@ -620,6 +620,16 @@ async function ensureFreshImage({ emit } = {}) {
620
620
  return true;
621
621
  }
622
622
 
623
+ // 容器内部视角的健康:docker exec 进容器用 node 探 127.0.0.1:8008。用来区分「容器真挂了」和
624
+ // 「容器好好的、只是 WSL 的 localhost 转发坏了」(后者 Windows 侧 :8008 打不通,实测
625
+ // `wsl --shutdown` 后 75 秒恢复)。
626
+ async function insideHealthy(container = "cicy-code-docker-8008", port = 8008) {
627
+ try {
628
+ await wslRun(`docker exec ${container} node -e "fetch('http://127.0.0.1:${port}/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"`, { timeout: 20000 });
629
+ return true;
630
+ } catch { return false; }
631
+ }
632
+
623
633
  // HTTP /health probe on 127.0.0.1:port from Windows — WSL2 forwards localhost,
624
634
  // so a container published on :port is reachable here. Reuse docker.js's probe.
625
635
  const probeHealth = docker.probeHealth;
@@ -1602,6 +1612,7 @@ async function readMihomoSelections(container = "cicy-code-docker-8008") {
1602
1612
  }
1603
1613
 
1604
1614
  module.exports = {
1615
+ insideHealthy, wslShutdown,
1605
1616
  bootstrap, status, restart, stop, dockerRestart, recreate, update, upgrade, runContainer, readContainerToken,
1606
1617
  distroInstalled, dockerInstalled, dockerEngineUp, imagePresent, probeHealth, wslRun, hasGatewayKey,
1607
1618
  readMihomoConfig, readMihomoSelections, parseMihomoSelections,
@@ -93,3 +93,17 @@ test("image load survives a missing /mnt/c mount (drvfs retry, then stdin pipe)"
93
93
  assert.match(w, /function loadImageViaStdin\(/);
94
94
  assert.match(w, /\["-d", DISTRO, "-u", "root", "--", "docker", "load"\]/);
95
95
  });
96
+
97
+ test("a broken component store is reported once and backed off for hours, not re-repaired every 15 min", () => {
98
+ const d = read("src/sidecar/docker.js");
99
+ assert.match(d, /wsl-repair-attempted\.txt/);
100
+ assert.match(d, /reason: storeBroken \? "windows_component_store_broken" : "wsl_enable_failed"/);
101
+ assert.match(read("src/backends/sidecar-ipc.js"), /windows_component_store_broken: 6 \* 60 \* 60 \* 1000/);
102
+ });
103
+
104
+ test("broken WSL localhost relay (healthy inside, unreachable from Windows) is reset once per boot", () => {
105
+ assert.match(read("src/sidecar/wsl-docker.js"), /async function insideHealthy\(/);
106
+ const ipc = read("src/backends/sidecar-ipc.js");
107
+ assert.match(ipc, /_relayMisses >= 2 && !_relayResetDone/);
108
+ assert.match(ipc, /docker:self-heal-localhost-relay/);
109
+ });
@@ -14,7 +14,7 @@ test("ensureWsl explains a disabled-virtualization host in the log and the card"
14
14
  assert.match(ensure, /reason: "virtualization_disabled"/);
15
15
  assert.match(ensure, /log\.error\(`\[bootstrap\] ✗ ensure-wsl reason=virtualization_disabled/);
16
16
  assert.match(ensure, /BIOS/);
17
- assert.match(ensure, /reason: "wsl_enable_failed", message/);
17
+ assert.match(ensure, /reason: storeBroken \? "windows_component_store_broken" : "wsl_enable_failed", message/);
18
18
  assert.match(read("src/sidecar/wsl-docker.js"), /const reason = w\.reason \|\| "wsl_enable_failed"; fail\(reason, w\.message\)/);
19
19
  const ipc = read("src/backends/sidecar-ipc.js");
20
20
  assert.match(ipc, /lastError: \(!s\.running && _lastBootstrapError\)/);