cicy-desktop 2.1.205 → 2.1.207
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 +6 -6
- package/src/app-updater.js +27 -1
- package/src/backends/homepage-preload.js +3 -0
- package/src/backends/homepage-react/assets/index-D_1u1626.js +377 -0
- package/src/backends/homepage-react/index.html +1 -1
- package/src/backends/sidecar-ipc.js +62 -2
- package/src/i18n/locales/en.json +36 -2
- package/src/i18n/locales/fr.json +36 -2
- package/src/i18n/locales/ja.json +36 -2
- package/src/i18n/locales/zh-CN.json +36 -2
- package/src/sidecar/container-scripts/cicy-code-update.sh +104 -0
- package/src/sidecar/wsl-docker.js +215 -22
- package/workers/render/src/App.jsx +140 -9
- package/src/backends/homepage-react/assets/index-Bf9kuVZb.js +0 -377
|
@@ -131,6 +131,72 @@ async function distroInstalled(distro = DISTRO) {
|
|
|
131
131
|
});
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// ── WSL 卡死(wedged)自愈 ────────────────────────────────────────────────────
|
|
135
|
+
// 偶发深度死锁:所有 `wsl` 命令(连 `wsl --shutdown`)都 hang,且重启电脑也复发——
|
|
136
|
+
// 成因 = 某 distro 的 vhdx 损坏 + LxssManager 卡在 STOP_PENDING。普通用户无从下手。
|
|
137
|
+
// 关键:`sc query LxssManager` / 注册表 / 文件操作都不走 wsl、不会 hang,所以 app 能
|
|
138
|
+
// 绕开卡死的 wsl 自检 + 自愈:检测到卡死 → 标记 → (重启后)bootstrap 启动时自动把坏
|
|
139
|
+
// distro 从注册表移除 + 文件夹改名备份(不删,留 vhdx 可 `wsl --mount` 抢救)→ 重新导入。
|
|
140
|
+
const WSL_RESET_FLAG = path.join(os.homedir(), "cicy-ai", "db", "wsl-reset-needed.flag");
|
|
141
|
+
function markWslReset() { try { fs.mkdirSync(path.dirname(WSL_RESET_FLAG), { recursive: true }); fs.writeFileSync(WSL_RESET_FLAG, String(Date.now())); } catch {} }
|
|
142
|
+
function wslResetPending() { try { return fs.existsSync(WSL_RESET_FLAG); } catch { return false; } }
|
|
143
|
+
function clearWslReset() { try { fs.unlinkSync(WSL_RESET_FLAG); } catch {} }
|
|
144
|
+
|
|
145
|
+
// LxssManager 卡在 STOP_PENDING/START_PENDING = WSL wedged(此刻所有 wsl 命令都 hang)。
|
|
146
|
+
// 用 `sc query`(不经 wsl,秒回)判定,绝不 hang。
|
|
147
|
+
function lxssWedged() {
|
|
148
|
+
if (process.platform !== "win32") return Promise.resolve(false);
|
|
149
|
+
return new Promise((resolve) => {
|
|
150
|
+
execFile("sc", ["query", "LxssManager"], { timeout: 8000, windowsHide: true }, (err, stdout) => {
|
|
151
|
+
if (err) return resolve(false);
|
|
152
|
+
resolve(/STOP_PENDING|START_PENDING/i.test(String(stdout || "")));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 不经 wsl 移除坏 distro:删注册表 Lxss 项 + BasePath 文件夹改名备份(.bak-<ts>,不删)。
|
|
158
|
+
// 仅当 distro 未挂载(vhdx 未锁)时成功——即重启后、任何 wsl 调用之前。返回 powershell 输出。
|
|
159
|
+
function resetDistroFiles(distro = DISTRO) {
|
|
160
|
+
const ps = `$ErrorActionPreference='SilentlyContinue'
|
|
161
|
+
$b='HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Lxss'
|
|
162
|
+
Get-ChildItem $b | ForEach-Object {
|
|
163
|
+
$p = Get-ItemProperty $_.PSPath
|
|
164
|
+
if ($p.DistributionName -eq '${distro}') {
|
|
165
|
+
$bp = $p.BasePath
|
|
166
|
+
if ($bp -and $bp.StartsWith('\\\\?\\')) { $bp = $bp.Substring(4) }
|
|
167
|
+
Remove-Item $_.PSPath -Recurse -Force
|
|
168
|
+
if ($bp -and (Test-Path $bp)) { Move-Item $bp ($bp + '.bak-' + (Get-Date -Format yyyyMMddHHmmss)) -Force }
|
|
169
|
+
Write-Output 'reset-done'
|
|
170
|
+
}
|
|
171
|
+
}`;
|
|
172
|
+
const file = path.join(os.tmpdir(), `cicy-wsl-reset-${Date.now()}.ps1`);
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
try { fs.writeFileSync(file, ps); } catch {}
|
|
175
|
+
execFile("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", file], { timeout: 30000, windowsHide: true }, (err, so) => {
|
|
176
|
+
try { fs.unlinkSync(file); } catch {}
|
|
177
|
+
resolve(String(so || "").trim());
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 一键修复(app 调):先试重启 LxssManager;还卡死(深度死锁,杀不动)→ 标记待重置 +
|
|
183
|
+
// 告诉调用方需重启电脑(重启后 bootstrap 自动重置坏 distro)。
|
|
184
|
+
async function repairWsl({ emit } = {}) {
|
|
185
|
+
if (process.platform !== "win32") return { ok: false, error: "win-only" };
|
|
186
|
+
const say = (m) => { try { emit && emit({ phase: "install-docker", status: "running", message: m }); } catch {} };
|
|
187
|
+
say("尝试重启 WSL 服务(LxssManager)…");
|
|
188
|
+
await new Promise((r) => execFile("powershell", ["-NoProfile", "-Command", "Restart-Service LxssManager -Force -ErrorAction SilentlyContinue"], { timeout: 30000, windowsHide: true }, () => r()));
|
|
189
|
+
await new Promise((r) => setTimeout(r, 2500));
|
|
190
|
+
if (await lxssWedged()) {
|
|
191
|
+
markWslReset();
|
|
192
|
+
say("WSL 服务仍卡死,需要重启电脑;重启后会自动修复。");
|
|
193
|
+
return { ok: false, needsReboot: true };
|
|
194
|
+
}
|
|
195
|
+
say("WSL 服务已恢复,正在重置环境…");
|
|
196
|
+
markWslReset(); // 坏 distro 仍可能在,交给(可能需重启后的)bootstrap 自动重置
|
|
197
|
+
return { ok: true };
|
|
198
|
+
}
|
|
199
|
+
|
|
134
200
|
// Install the Ubuntu distro WITHOUT launching its interactive first-run setup
|
|
135
201
|
// (--no-launch). We always run commands as root afterwards, so no user account
|
|
136
202
|
// is needed. Elevated via the scheduled-task path (reliable on these machines).
|
|
@@ -331,7 +397,7 @@ async function launchDockerd() {
|
|
|
331
397
|
{ timeout: 20000 });
|
|
332
398
|
}
|
|
333
399
|
|
|
334
|
-
async function startEngine({ emit } = {}) {
|
|
400
|
+
async function startEngine({ emit, noShutdown = false } = {}) {
|
|
335
401
|
const e = (ev) => { try { emit && emit(ev); } catch {} };
|
|
336
402
|
// Up to 3 attempts. Each: launch dockerd (detached, returns instantly) then POLL
|
|
337
403
|
// the socket in SHORT, SEPARATE wsl calls — never hold the distro in one 120s call
|
|
@@ -358,7 +424,10 @@ async function startEngine({ emit } = {}) {
|
|
|
358
424
|
// Recover. A stuck launch = WSL wedged → `wsl --shutdown` (full VM reset — the
|
|
359
425
|
// ONLY thing that clears a real wedge; --terminate isn't enough). Otherwise
|
|
360
426
|
// dockerd just died/is slow → clear stale runtime files for a clean relaunch.
|
|
361
|
-
if (stuck
|
|
427
|
+
if (stuck && noShutdown) {
|
|
428
|
+
// 更新路径:绝不 wsl --shutdown(会把正在跑的容器 + 整个 VM 干掉)。只记录,交给上层处理。
|
|
429
|
+
log.warn(`[startEngine] WSL stuck but noShutdown=true → 跳过 wsl --shutdown(更新路径不重置 VM)`);
|
|
430
|
+
} else if (stuck) { log.info(`[startEngine] WSL stuck → wsl --shutdown`); try { await wslShutdown(); } catch {} }
|
|
362
431
|
else { try { await wslRun("if ! pgrep dockerd >/dev/null 2>&1; then rm -f /var/run/docker.pid /run/docker.pid /var/run/docker.sock /run/docker.sock; fi", { timeout: 15000 }); } catch {} }
|
|
363
432
|
}
|
|
364
433
|
log.error("[startEngine] ✗ dockerd NOT up after 3 attempts");
|
|
@@ -474,7 +543,22 @@ function projectsMountArg() {
|
|
|
474
543
|
} catch (e) { log.warn(`[wsl-docker] projects mount setup failed: ${e.message}`); return ""; }
|
|
475
544
|
}
|
|
476
545
|
|
|
477
|
-
|
|
546
|
+
// extraPorts → additional `-p 127.0.0.1:<p>:<p>` mappings (host=container, loopback
|
|
547
|
+
// only). For container-internal agent services the user wants reachable from
|
|
548
|
+
// Windows. Each adds one docker-proxy, so keep the list small. 8008/主端口自动排除。
|
|
549
|
+
function publishArgs(port, extraPorts = []) {
|
|
550
|
+
const seen = new Set([Number(port), 8008]);
|
|
551
|
+
let args = `-p 127.0.0.1:${port}:8008`;
|
|
552
|
+
for (const raw of Array.isArray(extraPorts) ? extraPorts : []) {
|
|
553
|
+
const p = Number(raw);
|
|
554
|
+
if (!Number.isInteger(p) || p < 1 || p > 65535 || seen.has(p)) continue;
|
|
555
|
+
seen.add(p);
|
|
556
|
+
args += ` -p 127.0.0.1:${p}:${p}`;
|
|
557
|
+
}
|
|
558
|
+
return args;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function runContainer({ port = 8008, container = "cicy-code-docker", volume = "cicy-team-8008", env = {}, extraPorts = [], emit } = {}) {
|
|
478
562
|
// 每次容器"启动"(含已在跑被 adopt)都确保桌面快捷方式存在 —— 不存在就建,坏了就修。
|
|
479
563
|
if (await probeHealth(port)) { ensureDesktopShortcut(volume, port).catch(() => {}); return { adopted: true }; }
|
|
480
564
|
// Replace any stale same-named container.
|
|
@@ -493,7 +577,7 @@ async function runContainer({ port = 8008, container = "cicy-code-docker", volum
|
|
|
493
577
|
// 只发布 :8008(单端口,1 个 docker-proxy)。EXTRA_PORTS 那段 18000-19999(2000 个端口)
|
|
494
578
|
// 已删——docker 默认每端口起一个 userland-proxy 进程 → 2000 进程,docker run 卡死/吃内存/
|
|
495
579
|
// 偶发失败(实测)。容器内 agent 服务需要从 Windows 直达时再按需单独暴露。
|
|
496
|
-
const cmd = `docker run -d --name ${container} --restart unless-stopped --dns 223.5.5.5 --dns 8.8.8.8
|
|
580
|
+
const cmd = `docker run -d --name ${container} --restart unless-stopped --dns 223.5.5.5 --dns 8.8.8.8 ${publishArgs(port, extraPorts)} -e CICY_PUBLIC=1 -v ${volume}:/home/cicy ${projectsMountArg()} ${envArgs} ${IMAGE}`;
|
|
497
581
|
emit && emit({ phase: "container", status: "running", message: `$ ${cmd.length > 220 ? cmd.slice(0, 220) + " …" : cmd}` });
|
|
498
582
|
await wslRun(cmd, { timeout: 60000 }); // 失败时 err.stderr 带 docker 真错误 → _bootstrap 的 errTail 显示
|
|
499
583
|
ensureDesktopShortcut(volume, port).catch(() => {});
|
|
@@ -615,7 +699,13 @@ async function status(port = 8008) {
|
|
|
615
699
|
const running = healthy || !!(engineUp && (await probeHealth(port)));
|
|
616
700
|
// 服务在跑(:8008 健康)但 wsl 查不到发行版/引擎 → WSL 被孤儿化,管理(更新/重启)会失败。
|
|
617
701
|
const wslUnmanaged = healthy && !engineUp;
|
|
618
|
-
|
|
702
|
+
// WSL 卡死(wedged):wsl 探测超时(unknown)且 :8008 也不健康,再确认 LxssManager 卡在
|
|
703
|
+
// STOP_PENDING(sc query 不 hang)→ 这是死锁,不是「没装」。标记待重置,卡片给「修复」。
|
|
704
|
+
let wslWedged = false;
|
|
705
|
+
if (unknown && !healthy) {
|
|
706
|
+
if (await lxssWedged()) { wslWedged = true; markWslReset(); }
|
|
707
|
+
}
|
|
708
|
+
return { wsl, distro, engineUp, running, unknown: unknown && !healthy, healthy, wslUnmanaged, wslWedged };
|
|
619
709
|
}
|
|
620
710
|
|
|
621
711
|
// Guard against overlapping bootstrap runs (double-click 重试 / re-entrancy):
|
|
@@ -652,7 +742,7 @@ async function bootstrap(opts = {}) {
|
|
|
652
742
|
// 注意: 默认容器/卷名保持 cicy-team / cicy-code-docker(回退实测"现在不行了"的改动)。
|
|
653
743
|
// live 路径(docker:app-bootstrap)始终传显式 APP_*(cicy-team-8008)名,不靠这里的默认;
|
|
654
744
|
// 改默认会让既有 cicy-team 卷的装机对不上 → 退回原值。
|
|
655
|
-
async function _bootstrap({ onProgress, port = 8008, container = "cicy-code-docker", volume = "cicy-team", env = {} } = {}) {
|
|
745
|
+
async function _bootstrap({ onProgress, port = 8008, container = "cicy-code-docker", volume = "cicy-team", env = {}, extraPorts = [] } = {}) {
|
|
656
746
|
const emit = (ev) => { try { onProgress && onProgress(ev); } catch {} };
|
|
657
747
|
|
|
658
748
|
// Structured, PERSISTED trace of the whole run (electron-log → main.log) so a
|
|
@@ -673,6 +763,17 @@ async function _bootstrap({ onProgress, port = 8008, container = "cicy-code-dock
|
|
|
673
763
|
};
|
|
674
764
|
log.info(`[bootstrap] START port=${port} container=${container} volume=${volume}`);
|
|
675
765
|
|
|
766
|
+
// 自愈:上次检测到 WSL 卡死(或修复流程标记了待重置)→ 此刻(重启后、未碰 wsl 前)把坏
|
|
767
|
+
// distro 从注册表移除 + 文件夹改名备份(不删数据),让下面走全新导入,而不是又去碰坏 distro
|
|
768
|
+
// 再次卡死。纯注册表/文件操作,不经 wsl。
|
|
769
|
+
if (process.platform === "win32" && wslResetPending()) {
|
|
770
|
+
begin("reset-broken-distro");
|
|
771
|
+
emit({ phase: "install-docker", status: "running", message: "检测到上次 WSL 卡死,正在重置环境(旧数据已改名备份,不删)…" });
|
|
772
|
+
try { const r = await resetDistroFiles(); log.info(`[bootstrap] reset-broken-distro out=${r || "(none)"}`); } catch (e) { log.warn(`[bootstrap] reset failed: ${e.message}`); }
|
|
773
|
+
clearWslReset();
|
|
774
|
+
done();
|
|
775
|
+
}
|
|
776
|
+
|
|
676
777
|
// 0) Fast path: healthy AND the distro is TRULY wsl-managed → instant no-op.
|
|
677
778
|
// BUT a ZOMBIE :8008 (distro unregistered from WSL, yet a leftover wslhost.exe
|
|
678
779
|
// still holds the port-forward → health 200 even after `wsl --shutdown`) is NOT
|
|
@@ -752,7 +853,7 @@ async function _bootstrap({ onProgress, port = 8008, container = "cicy-code-dock
|
|
|
752
853
|
begin("run-container");
|
|
753
854
|
if (!(await probeHealth(port))) {
|
|
754
855
|
emit({ phase: "container", status: "running", message: "启动 cicy-code 服务…" });
|
|
755
|
-
try { await runContainer({ port, container, volume, env, emit }); }
|
|
856
|
+
try { await runContainer({ port, container, volume, env, extraPorts, emit }); }
|
|
756
857
|
catch (e) { fail("container_start_failed", e.message); emit({ phase: "container", status: "error", message: `服务启动失败:${e.message}(点重试)${errTail(e)}` }); finish(false, "container_start_failed"); return { ok: false, reason: "container_start_failed" }; }
|
|
757
858
|
done();
|
|
758
859
|
} else done(true);
|
|
@@ -773,7 +874,9 @@ async function _bootstrap({ onProgress, port = 8008, container = "cicy-code-dock
|
|
|
773
874
|
// running (that's the whole point of the supervisor layout). Falls back to a
|
|
774
875
|
// full container restart on the pre-supervisor image.
|
|
775
876
|
async function restart({ container = "cicy-code-docker", port = 8008, volume = "cicy-team-8008" } = {}) {
|
|
776
|
-
|
|
877
|
+
// 重启 cicy-code 只需 docker exec——引擎必然在跑。同 update:绝不无条件 startEngine()
|
|
878
|
+
// (它误判卡住会 wsl --shutdown 把容器+WSL 干掉)。引擎确实没起才轻量拉,且 noShutdown。
|
|
879
|
+
if (!(await dockerEngineUp())) { await startEngine({ noShutdown: true }); }
|
|
777
880
|
try {
|
|
778
881
|
await wslRun(`docker exec ${container} supervisorctl -c /etc/supervisor/supervisord.conf restart cicy-code`, { timeout: 30000 });
|
|
779
882
|
} catch {
|
|
@@ -784,24 +887,109 @@ async function restart({ container = "cicy-code-docker", port = 8008, volume = "
|
|
|
784
887
|
return ok;
|
|
785
888
|
}
|
|
786
889
|
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
|
|
790
|
-
|
|
890
|
+
// Minimal JSON GET (host network, fail-fast). Used to resolve the latest
|
|
891
|
+
// cicy-code version on the HOST instead of inside the slow container registry.
|
|
892
|
+
function getJson(url, timeout = 12000) {
|
|
893
|
+
const https = require("https");
|
|
894
|
+
return new Promise((resolve, reject) => {
|
|
895
|
+
// Accept-Encoding: identity — Node http(s) does NOT auto-gunzip; force plain
|
|
896
|
+
// so JSON.parse never chokes on a compressed body. rejectUnauthorized:false —
|
|
897
|
+
// this only reads a version *string*, so survive TLS-intercepting proxies.
|
|
898
|
+
const req = https.get(url, { headers: { "User-Agent": "cicy-desktop", Accept: "application/json", "Accept-Encoding": "identity" }, timeout, rejectUnauthorized: false }, (res) => {
|
|
899
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { res.resume(); return getJson(res.headers.location, timeout).then(resolve, reject); }
|
|
900
|
+
if (res.statusCode !== 200) { res.resume(); return reject(new Error(`HTTP ${res.statusCode}`)); }
|
|
901
|
+
let s = ""; res.setEncoding("utf8"); res.on("data", (d) => (s += d));
|
|
902
|
+
res.on("end", () => { try { resolve(JSON.parse(s)); } catch (e) { reject(e); } });
|
|
903
|
+
});
|
|
904
|
+
req.on("error", reject);
|
|
905
|
+
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Resolve cicy-code's latest published version from the HOST (fast/reliable
|
|
910
|
+
// network), trying the network-appropriate registry first then falling back.
|
|
911
|
+
// 2 passes (transient failures are common), both registries each pass. Returns
|
|
912
|
+
// null if all fail (caller then lets the in-container script try).
|
|
913
|
+
async function resolveLatestCicy(net) {
|
|
914
|
+
const CN = "https://registry.npmmirror.com", OFFICIAL = "https://registry.npmjs.org";
|
|
915
|
+
const order = net === "global" ? [OFFICIAL, CN] : [CN, OFFICIAL];
|
|
916
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
917
|
+
for (const reg of order) {
|
|
918
|
+
try {
|
|
919
|
+
const j = await getJson(`${reg}/cicy-code/latest`, 12000);
|
|
920
|
+
if (j && j.version) return String(j.version);
|
|
921
|
+
} catch (e) { log.warn(`[wsl-docker] resolveLatestCicy via ${reg} (pass ${pass}) failed:`, e.message); }
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Current installed cicy-code version, read from the container's versions.json.
|
|
928
|
+
async function installedCicyVersion(container) {
|
|
929
|
+
try {
|
|
930
|
+
const { stdout } = await wslRun(`docker exec ${container} bash -lc "cat ~/cicy-ai/runtime/versions.json 2>/dev/null"`, { timeout: 15000 });
|
|
931
|
+
const j = JSON.parse(stdout);
|
|
932
|
+
return j && j["cicy-code"] && j["cicy-code"].current ? String(j["cicy-code"].current) : null;
|
|
933
|
+
} catch { return null; }
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Update cicy-code IN PLACE. SMART path (避免容器内 npm view 卡 ~2min):
|
|
937
|
+
// 1. 在宿主机解析最新版本号(host 网络,不过容器代理/DNS)
|
|
938
|
+
// 2. 和容器里已装版本比对——一样就直接「已是最新」,根本不 docker exec
|
|
939
|
+
// 3. 真有新版才 cp 脚本进容器,并把**已解析的具体版本**传给它(脚本跳过自己的 npm view)
|
|
940
|
+
// 容器里只剩真正的 `npm install -g cicy-code@<ver>`(+ fail-fast/换源),不再有版本查询的卡顿。
|
|
791
941
|
async function update({ onProgress, container = "cicy-code-docker", port = 8008 } = {}) {
|
|
792
942
|
const emit = (ev) => { try { onProgress && onProgress(ev); } catch {} };
|
|
793
|
-
|
|
794
|
-
|
|
943
|
+
// 更新一个 running 容器只需 docker exec——引擎必然在跑。绝不无条件 startEngine():它的
|
|
944
|
+
// 「WSL 卡住→wsl --shutdown」恢复路径会把正在跑的容器 + 整个 WSL VM 一起干掉(这正是
|
|
945
|
+
// 「点更新 → docker 挂 + WSL 卡死」的根因:一次瞬时 wsl 慢响应被误判成 wedged → shutdown)。
|
|
946
|
+
// 只有引擎确实没起才轻量拉一下,且**禁用 wsl --shutdown**(noShutdown)。
|
|
947
|
+
if (!(await dockerEngineUp())) {
|
|
948
|
+
emit({ phase: "container", status: "running", message: t("dockerOpen.engineStartingNoReset") });
|
|
949
|
+
await startEngine({ emit, noShutdown: true });
|
|
950
|
+
}
|
|
951
|
+
// 1) 宿主机解析最新版 + 读容器已装版本(都很快;net-detect 决定源顺序 + 注入)。
|
|
952
|
+
emit({ phase: "image", status: "running", message: t("docker.updating.checking") });
|
|
953
|
+
let net = "unknown";
|
|
954
|
+
try { net = await require("./net-detect").detect(); } catch {}
|
|
955
|
+
const [latest, current] = await Promise.all([resolveLatestCicy(net), installedCicyVersion(container)]);
|
|
956
|
+
log.info(`[wsl-docker] update: net=${net} latest=${latest} current=${current}`);
|
|
957
|
+
// 2) 已是最新 → 秒回,根本不进容器装(你点更新等 2 分钟的就是这种「其实已最新」的空跑)。
|
|
958
|
+
if (latest && current && latest === current) {
|
|
959
|
+
emit({ phase: "done", status: "done", message: t("docker.updating.alreadyLatest", { v: current }) });
|
|
960
|
+
return { ok: true, alreadyLatest: true, version: current };
|
|
961
|
+
}
|
|
962
|
+
// 宿主机没解析出版本 → 给个可见提示(诊断:让用户/我们知道是 host 网络问题,而非容器)。
|
|
963
|
+
if (!latest) emit({ phase: "image", status: "running", message: t("docker.updating.hostResolveFail") });
|
|
964
|
+
// 3) 真要装:cp desktop 自带的脚本进容器(随 desktop 发版下发,不依赖镜像),把**已解析
|
|
965
|
+
// 的具体版本**作参数传进去 → 脚本跳过自己的 npm view,容器里不再有版本查询的卡顿。
|
|
966
|
+
emit({ phase: "image", status: "running", message: latest ? t("docker.updating.toVersion", { v: latest }) : t("docker.updating.pulling") });
|
|
967
|
+
// cp 结果出到 drawer(诊断:之前静默失败 → 回落镜像内旧脚本 → 又卡 2 分钟,看不出来)。
|
|
795
968
|
try {
|
|
796
|
-
|
|
969
|
+
const b64 = fs.readFileSync(path.join(__dirname, "container-scripts", "cicy-code-update.sh")).toString("base64");
|
|
970
|
+
await wslRun(`echo ${b64} | base64 -d | docker exec -i ${container} bash -c 'cat > /usr/local/bin/cicy-code-update.sh && chmod 0755 /usr/local/bin/cicy-code-update.sh'`, { timeout: 30000 });
|
|
971
|
+
emit({ phase: "image", status: "running", message: t("docker.updating.scriptReady") });
|
|
972
|
+
} catch (e) {
|
|
973
|
+
log.warn("[wsl-docker] push cicy-code-update.sh failed, fallback to baked:", e.message);
|
|
974
|
+
emit({ phase: "image", status: "running", message: t("docker.updating.scriptPushFail") });
|
|
975
|
+
}
|
|
976
|
+
// 按宿主机网络选 npm 源(脚本里 NPM_REGISTRY override 最高优先)。
|
|
977
|
+
let regEnv = "";
|
|
978
|
+
if (net === "global") regEnv = "-e NPM_REGISTRY=https://registry.npmjs.org ";
|
|
979
|
+
else if (net === "cn") regEnv = "-e NPM_REGISTRY=https://registry.npmmirror.com ";
|
|
980
|
+
// 解析到了具体版本就传进去(脚本据此跳过 npm view);没解析到则传空 → 脚本走 latest+fail-fast。
|
|
981
|
+
const verArg = latest ? ` ${latest}` : "";
|
|
982
|
+
try {
|
|
983
|
+
await wslRunStream(`docker exec ${regEnv}${container} bash -lc "command -v cicy-code-update.sh >/dev/null && cicy-code-update.sh${verArg} || /usr/local/bin/cicy-code-update.sh${verArg}"`,
|
|
797
984
|
{ emit, phase: "image", timeout: 300000 });
|
|
798
985
|
} catch (e) {
|
|
799
|
-
emit({ phase: "done", status: "error", message:
|
|
986
|
+
emit({ phase: "done", status: "error", message: t("docker.updating.failed", { msg: e.message }) });
|
|
800
987
|
return { ok: false, reason: "update_failed" };
|
|
801
988
|
}
|
|
802
989
|
const healthy = await docker.waitUntil(() => probeHealth(port), { totalMs: 120000, everyMs: 3000 });
|
|
803
|
-
|
|
804
|
-
|
|
990
|
+
const doneMsg = healthy ? (latest ? t("docker.updating.doneVersion", { v: latest }) : t("docker.updating.doneLatest")) : t("docker.updating.notReady");
|
|
991
|
+
emit({ phase: "done", status: healthy ? "done" : "error", message: doneMsg });
|
|
992
|
+
return { ok: healthy, version: latest || null };
|
|
805
993
|
}
|
|
806
994
|
async function stop({ container = "cicy-code-docker" } = {}) {
|
|
807
995
|
try { await wslRun(`docker stop ${container}`, { timeout: 30000 }); } catch {}
|
|
@@ -818,16 +1006,21 @@ async function dockerRestart({ container = "cicy-code-docker-8008" } = {}) {
|
|
|
818
1006
|
// 重建容器:docker rm -f 旧容器 + docker run 新容器(用新 env,如新的 docker team 网关
|
|
819
1007
|
// key)。**保留 volume**(数据/api_token/deviceId 不丢),只是换掉容器本身 + env。
|
|
820
1008
|
// 破坏性(短暂中断 + 换 key)→ 调用方要 confirm。
|
|
821
|
-
async function recreate({ onProgress, port = 8008, container = "cicy-code-docker-8008", volume = "cicy-team-8008", env = {} } = {}) {
|
|
1009
|
+
async function recreate({ onProgress, port = 8008, container = "cicy-code-docker-8008", volume = "cicy-team-8008", env = {}, extraPorts = [] } = {}) {
|
|
822
1010
|
const emit = (ev) => { try { onProgress && onProgress(ev); } catch {} };
|
|
823
1011
|
// 重建 = 用最新镜像重建。OSS 有更新版先刷新(非破坏性,不删发行版/volume),再 rm + run。
|
|
824
1012
|
try { await ensureFreshImage({ emit }); } catch (e) { emit({ phase: "image", status: "running", message: `镜像刷新跳过(${e.message}),用现有镜像重建` }); }
|
|
825
1013
|
// 强删占用该端口的**任何**容器(含老名字 cicy-code-docker)+ 目标容器 —— 否则
|
|
826
1014
|
// runContainer 开头的 probeHealth 看到旧容器还健康会 adopt 它、不重建,key 就换不了。
|
|
827
1015
|
try { await wslRun(`docker ps -aq --filter publish=${port} | xargs -r docker rm -f 2>/dev/null; docker rm -f ${container} 2>/dev/null; true`, { timeout: 30000 }); } catch {}
|
|
828
|
-
const r = await runContainer({ port, container, volume, env, emit });
|
|
1016
|
+
const r = await runContainer({ port, container, volume, env, extraPorts, emit });
|
|
829
1017
|
try { await ensureDesktopShortcut(volume, port); } catch {}
|
|
830
|
-
|
|
1018
|
+
// 等 :8008 真正起来再返回——否则 docker run 一返回卡片就探测「没运行」,显示「启动」让
|
|
1019
|
+
// 用户再点一下(实测:端口保存后重建,drawer 关了还要手动点启动)。
|
|
1020
|
+
emit({ phase: "container", status: "running", message: t("docker.updating.starting") });
|
|
1021
|
+
const healthy = await docker.waitUntil(() => probeHealth(port), { totalMs: 120000, everyMs: 3000 });
|
|
1022
|
+
emit({ phase: "done", status: healthy ? "done" : "error", message: healthy ? t("docker.ready") : t("docker.updating.notReady") });
|
|
1023
|
+
return { ...r, ok: healthy };
|
|
831
1024
|
}
|
|
832
1025
|
// Unregister the dedicated distro (idempotent; no-op if absent). Used by upgrade
|
|
833
1026
|
// to wipe a stale install before re-importing the latest pre-baked package.
|
|
@@ -867,5 +1060,5 @@ async function readMihomoConfig(container = "cicy-code-docker-8008") {
|
|
|
867
1060
|
module.exports = {
|
|
868
1061
|
bootstrap, status, restart, stop, dockerRestart, recreate, update, upgrade, runContainer, readContainerToken,
|
|
869
1062
|
distroInstalled, dockerInstalled, dockerEngineUp, imagePresent, probeHealth, wslRun, hasGatewayKey,
|
|
870
|
-
readMihomoConfig,
|
|
1063
|
+
readMihomoConfig, repairWsl, lxssWedged,
|
|
871
1064
|
};
|
|
@@ -1155,7 +1155,7 @@ function TrustedSitesModal({ onClose }) {
|
|
|
1155
1155
|
};
|
|
1156
1156
|
|
|
1157
1157
|
return createPortal(
|
|
1158
|
-
<div style={S.overlay}
|
|
1158
|
+
<div style={S.overlay} onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }} data-id="TrustedSitesModal">
|
|
1159
1159
|
<div style={S.card} onClick={(e) => e.stopPropagation()}>
|
|
1160
1160
|
<div style={S.head}>
|
|
1161
1161
|
<h2 style={S.title}>{tr("trustedSites.title", "受信任站点")}</h2>
|
|
@@ -1285,7 +1285,7 @@ function AuditLogModal({ onClose }) {
|
|
|
1285
1285
|
const Th = (t) => <div>{t}</div>;
|
|
1286
1286
|
|
|
1287
1287
|
return createPortal(
|
|
1288
|
-
<div style={S.overlay}
|
|
1288
|
+
<div style={S.overlay} onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }} data-id="AuditLogModal">
|
|
1289
1289
|
<div style={S.card} onClick={(e) => e.stopPropagation()}>
|
|
1290
1290
|
<div style={S.head}>
|
|
1291
1291
|
<div style={S.titleWrap}>
|
|
@@ -1822,8 +1822,15 @@ const dockerDrawer = {
|
|
|
1822
1822
|
// Log only milestone events — never the per-% running download ticks.
|
|
1823
1823
|
const isRunningTick = ev.status === "running" && hasPct && isDl;
|
|
1824
1824
|
if (!isRunningTick) {
|
|
1825
|
-
|
|
1826
|
-
|
|
1825
|
+
// 去重:两条订阅(全局兜底 + 某 run*() 自己的)偶发对同一事件各 push 一次 → 日志重复。
|
|
1826
|
+
// 末行 phase/status/message 完全相同则跳过(兜底,不影响正常重复行很少的场景)。
|
|
1827
|
+
const last = dockerDrawerState.logs[dockerDrawerState.logs.length - 1];
|
|
1828
|
+
const st = ev.status || "running";
|
|
1829
|
+
const msg = ev.message || "";
|
|
1830
|
+
if (!(last && last.phase === phase && last.status === st && last.message === msg)) {
|
|
1831
|
+
const line = { id: ++dockerDrawerLogSeq, t: clockHHMMSS(), phase, status: st, message: msg };
|
|
1832
|
+
next.logs = [...dockerDrawerState.logs, line];
|
|
1833
|
+
}
|
|
1827
1834
|
}
|
|
1828
1835
|
dockerDrawerState = next;
|
|
1829
1836
|
emitDockerDrawer();
|
|
@@ -1993,6 +2000,10 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
1993
2000
|
const [busy, setBusy] = useState(""); // "" | bootstrap | restart | stop | upgrade | probe
|
|
1994
2001
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
1995
2002
|
const [confirmRecreate, setConfirmRecreate] = useState(false); // 重建容器 in-app 确认弹窗(不用 native confirm)
|
|
2003
|
+
const [portsOpen, setPortsOpen] = useState(false); // 端口设置 modal
|
|
2004
|
+
const [portList, setPortList] = useState([]); // 编辑中的额外端口(字符串数组,便于输入)
|
|
2005
|
+
const [portsBusy, setPortsBusy] = useState(false);
|
|
2006
|
+
const [portsErr, setPortsErr] = useState("");
|
|
1996
2007
|
// Inline rename (mirrors LocalTeamCard): double-click the title to edit.
|
|
1997
2008
|
const [editing, setEditing] = useState(false);
|
|
1998
2009
|
const [draft, setDraft] = useState("");
|
|
@@ -2021,7 +2032,12 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
2021
2032
|
// 不到 docker:app-progress 完成事件,抽屉就一直转(用户看到的「正在跟随同一进度」假死)。
|
|
2022
2033
|
// 这里检测到容器起来了就直接把抽屉收成「完成」,不再死等 promise。
|
|
2023
2034
|
// kind!=="open":只自愈安装抽屉;「打开」抽屉(失败报告)绝不被「已就绪」劫持。
|
|
2024
|
-
|
|
2035
|
+
// **只自愈真正卡住的抽屉**(>10s 没收到任何进度事件):否则会误杀正在进行的「更新/
|
|
2036
|
+
// 重启 cicy-code」——它们是 in-place 操作,容器全程 healthy,点更新瞬间 busy 变化触发
|
|
2037
|
+
// 一次 checkStatus 就会把还在跑的抽屉提前收成「完成」,用户点完成关掉、命令却还在跑、
|
|
2038
|
+
// 卡片卡在「处理中」(实测 bug)。活跃流式的抽屉 lastAt 是新的,不动它。
|
|
2039
|
+
const stale = Date.now() - (dockerDrawerState?.lastAt || 0) > 30000;
|
|
2040
|
+
if (s?.running && dockerDrawerState && dockerDrawerState.status === "running" && dockerDrawerState.kind !== "open" && stale) {
|
|
2025
2041
|
dockerDrawer.finish({ ok: true, message: "Docker cicy-code 已就绪" });
|
|
2026
2042
|
}
|
|
2027
2043
|
} catch (e) { console.warn("[DockerCard]", e); }
|
|
@@ -2036,6 +2052,38 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
2036
2052
|
return () => clearInterval(id);
|
|
2037
2053
|
}, [checkStatus, busy]);
|
|
2038
2054
|
|
|
2055
|
+
// 端口设置(定义在 checkStatus 之后:savePorts 依赖它,放前面会 TDZ 崩首页)。
|
|
2056
|
+
const openPorts = useCallback(async () => {
|
|
2057
|
+
setMenuOpen(false); setPortsErr("");
|
|
2058
|
+
try { const r = await window.cicy?.docker?.getPorts?.(); setPortList((r?.ports || []).map(String)); }
|
|
2059
|
+
catch { setPortList([]); }
|
|
2060
|
+
setPortsOpen(true);
|
|
2061
|
+
}, []);
|
|
2062
|
+
const savePorts = useCallback(async () => {
|
|
2063
|
+
// 校验:1-65535、≠8008、去重、忽略空行。
|
|
2064
|
+
const seen = new Set(); const out = [];
|
|
2065
|
+
for (const raw of portList) {
|
|
2066
|
+
const s = String(raw).trim(); if (!s) continue;
|
|
2067
|
+
const p = Number(s);
|
|
2068
|
+
if (!Number.isInteger(p) || p < 1 || p > 65535 || p === 8008 || seen.has(p)) { setPortsErr(tr("docker.ports.invalid", "有端口无效(1-65535,不能是 8008,不能重复)")); return; }
|
|
2069
|
+
seen.add(p); out.push(p);
|
|
2070
|
+
}
|
|
2071
|
+
setPortsErr(""); setPortsBusy(true); setPortsOpen(false);
|
|
2072
|
+
setBusy("recreate");
|
|
2073
|
+
dockerDrawer.open({ onRetry: savePorts });
|
|
2074
|
+
const unsub = window.cicy?.docker?.onAppProgress?.((ev) => dockerDrawer.push(ev));
|
|
2075
|
+
try {
|
|
2076
|
+
const r = await window.cicy?.docker?.setPorts?.(out);
|
|
2077
|
+
dockerDrawer.finish({ ok: !!r?.ok, message: r?.ok ? tr("docker.ports.save", "保存并重建") + " ✅" : (r?.error || tr("docker.opFailed", "操作失败")) });
|
|
2078
|
+
if (r?.ok) { try { const s = await window.cicy?.docker?.appRedetect?.(); if (s) setStatus(s); } catch {} }
|
|
2079
|
+
} catch (e) {
|
|
2080
|
+
dockerDrawer.finish({ ok: false, message: e.message });
|
|
2081
|
+
} finally {
|
|
2082
|
+
try { unsub && unsub(); } catch {}
|
|
2083
|
+
setPortsBusy(false); setBusy(""); checkStatus();
|
|
2084
|
+
}
|
|
2085
|
+
}, [portList, checkStatus]);
|
|
2086
|
+
|
|
2039
2087
|
// Close the ⋯ menu on outside-click / Esc (mirrors LocalTeamCard).
|
|
2040
2088
|
useEffect(() => {
|
|
2041
2089
|
if (!menuOpen) return;
|
|
@@ -2081,6 +2129,30 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
2081
2129
|
}
|
|
2082
2130
|
}, [checkStatus, onRefresh]);
|
|
2083
2131
|
|
|
2132
|
+
// 修复 WSL(WSL 死锁时):重启 LxssManager;若深度死锁(杀不动)→ 提示重启电脑(重启后
|
|
2133
|
+
// bootstrap 自动重置坏 distro)。全程走抽屉显示进度。
|
|
2134
|
+
const repairWsl = useCallback(async () => {
|
|
2135
|
+
setBusy("repair");
|
|
2136
|
+
dockerDrawer.open({ onRetry: repairWsl });
|
|
2137
|
+
const unsub = window.cicy?.docker?.onAppProgress?.((ev) => dockerDrawer.push(ev));
|
|
2138
|
+
try {
|
|
2139
|
+
const r = await window.cicy?.docker?.appRepairWsl?.();
|
|
2140
|
+
if (r?.needsReboot) {
|
|
2141
|
+
dockerDrawer.finish({ status: "reboot", message: tr("docker.wslNeedReboot", "WSL 服务卡死,请【重启 Windows】;重启后打开 CiCy 会自动修复(旧数据已改名备份,不删)。") });
|
|
2142
|
+
} else if (r?.ok) {
|
|
2143
|
+
dockerDrawer.finish({ ok: true, message: tr("docker.wslRepaired", "WSL 已修复 ✅") });
|
|
2144
|
+
onRefresh?.();
|
|
2145
|
+
} else {
|
|
2146
|
+
dockerDrawer.finish({ ok: false, message: r?.error || tr("docker.repairFailed", "修复失败") });
|
|
2147
|
+
}
|
|
2148
|
+
} catch (e) {
|
|
2149
|
+
dockerDrawer.finish({ ok: false, message: e.message });
|
|
2150
|
+
} finally {
|
|
2151
|
+
try { unsub && unsub(); } catch {}
|
|
2152
|
+
setBusy(""); checkStatus();
|
|
2153
|
+
}
|
|
2154
|
+
}, [checkStatus, onRefresh]);
|
|
2155
|
+
|
|
2084
2156
|
// Upgrade: re-pull the R2 image + recreate the container — also through the
|
|
2085
2157
|
// drawer so the user sees the pull/import/restart log (升级要能看日志).
|
|
2086
2158
|
const runUpgrade = useCallback(async () => {
|
|
@@ -2181,10 +2253,13 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
2181
2253
|
// 消失 → token 读不到、打不开。所以 wslUnmanaged 不再是「可打开」,而是「需修复」:
|
|
2182
2254
|
// CTA 走「修复 WSL」自动重装(bootstrap 会杀僵尸端口 + wsl --shutdown + 重新 import)。
|
|
2183
2255
|
const wslUnmanaged = !!status?.wslUnmanaged;
|
|
2256
|
+
const wslWedged = !!status?.wslWedged; // WSL 死锁(LxssManager StopPending,所有 wsl 命令 hang)
|
|
2184
2257
|
const realRunning = running && !wslUnmanaged; // 真能打开 = 健康 且 WSL 没孤儿化
|
|
2185
|
-
const tone = realRunning ? "ok" : (wslUnmanaged || dockerRunning || installed || unknown) ? "warn" : "off";
|
|
2258
|
+
const tone = realRunning ? "ok" : (wslWedged || wslUnmanaged || dockerRunning || installed || unknown) ? "warn" : "off";
|
|
2186
2259
|
const isBusy = !!busy;
|
|
2187
|
-
const stateText =
|
|
2260
|
+
const stateText = wslWedged
|
|
2261
|
+
? tr("docker.wslWedged", "WSL 卡死 · 点「修复 WSL」")
|
|
2262
|
+
: wslUnmanaged
|
|
2188
2263
|
? tr("docker.wslBrokenRepair", "WSL 管理异常 · 点「修复 WSL」重装")
|
|
2189
2264
|
: running
|
|
2190
2265
|
? tr("docker.running", "运行中")
|
|
@@ -2204,7 +2279,7 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
2204
2279
|
? tr("docker.working", "处理中…")
|
|
2205
2280
|
: realRunning
|
|
2206
2281
|
? tr("localTeams.open", "打开")
|
|
2207
|
-
: wslUnmanaged
|
|
2282
|
+
: (wslWedged || wslUnmanaged)
|
|
2208
2283
|
? tr("docker.repairWsl", "修复 WSL")
|
|
2209
2284
|
: dockerRunning
|
|
2210
2285
|
? tr("docker.start", "启动")
|
|
@@ -2216,6 +2291,8 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
2216
2291
|
|
|
2217
2292
|
const onCta = async () => {
|
|
2218
2293
|
if (isBusy) return;
|
|
2294
|
+
// WSL 死锁 → 走「修复 WSL」(重启 LxssManager;不行则提示重启电脑 + 重启后自动重置坏 distro)。
|
|
2295
|
+
if (wslWedged) { repairWsl(); return; }
|
|
2219
2296
|
// WSL 孤儿化 → 走「修复」(bootstrap 杀僵尸端口 + wsl --shutdown + 重新 import),不进打开。
|
|
2220
2297
|
if (wslUnmanaged) { runBootstrap(); return; }
|
|
2221
2298
|
if (realRunning) {
|
|
@@ -2331,6 +2408,10 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
2331
2408
|
onClick={() => runOp("restart", () => window.cicy.docker.appDockerRestart(), tr("docker.dockerRestarted", "已重启 Docker 容器"))}>
|
|
2332
2409
|
{tr("docker.dockerRestart", "重启 Docker")}
|
|
2333
2410
|
</button>
|
|
2411
|
+
<button type="button" data-id="DockerCard-ports" className="bcard__menu-item"
|
|
2412
|
+
onClick={(e) => { e.stopPropagation(); openPorts(); }}>
|
|
2413
|
+
{tr("docker.ports.menu", "端口设置")}
|
|
2414
|
+
</button>
|
|
2334
2415
|
<button type="button" data-id="DockerCard-recreate" className="bcard__menu-item is-danger"
|
|
2335
2416
|
onClick={(e) => { e.stopPropagation(); setMenuOpen(false); setConfirmRecreate(true); }}>
|
|
2336
2417
|
{tr("docker.recreate", "重建 Docker")}
|
|
@@ -2385,7 +2466,7 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
2385
2466
|
{confirmRecreate && createPortal(
|
|
2386
2467
|
<div data-id="DockerCard-recreate-modal"
|
|
2387
2468
|
style={{ position: "fixed", inset: 0, zIndex: 9999, background: "rgba(0,0,0,0.55)", display: "flex", alignItems: "center", justifyContent: "center" }}
|
|
2388
|
-
|
|
2469
|
+
onMouseDown={(e) => { if (e.target === e.currentTarget) setConfirmRecreate(false); }}>
|
|
2389
2470
|
<div onClick={(e) => e.stopPropagation()}
|
|
2390
2471
|
style={{ width: 360, maxWidth: "90vw", background: "#161b22", border: "1px solid #30363d", borderRadius: 12, padding: "20px 22px", boxShadow: "0 12px 40px rgba(0,0,0,0.5)" }}>
|
|
2391
2472
|
<h3 style={{ margin: "0 0 8px", fontSize: 16, color: "#e6edf3" }}>{tr("docker.recreate", "重建 Docker")}</h3>
|
|
@@ -2408,6 +2489,56 @@ function DockerCard({ dockerTeam, cloudTitle, cloudCode, onOpen, onRename, onRef
|
|
|
2408
2489
|
</div>,
|
|
2409
2490
|
document.body
|
|
2410
2491
|
)}
|
|
2492
|
+
{portsOpen && createPortal(
|
|
2493
|
+
<div data-id="DockerCard-ports-modal"
|
|
2494
|
+
style={{ position: "fixed", inset: 0, zIndex: 9999, background: "rgba(0,0,0,0.55)", display: "flex", alignItems: "center", justifyContent: "center" }}
|
|
2495
|
+
onMouseDown={(e) => { if (e.target === e.currentTarget) setPortsOpen(false); }}>
|
|
2496
|
+
<div onClick={(e) => e.stopPropagation()}
|
|
2497
|
+
style={{ width: 420, maxWidth: "92vw", background: "#161b22", border: "1px solid #30363d", borderRadius: 12, padding: "20px 22px", boxShadow: "0 12px 40px rgba(0,0,0,0.5)" }}>
|
|
2498
|
+
<h3 style={{ margin: "0 0 6px", fontSize: 16, color: "#e6edf3" }}>{tr("docker.ports.title", "端口设置")}</h3>
|
|
2499
|
+
<p style={{ margin: "0 0 14px", fontSize: 12.5, lineHeight: 1.6, color: "#9aa4b2" }}>{tr("docker.ports.sub", "除 :8008 外,额外发布、可从 Windows 直达容器内服务的端口")}</p>
|
|
2500
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, padding: "8px 10px", marginBottom: 10, borderRadius: 8, background: "#0d1117", border: "1px solid #21262d", color: "#7d8590", fontSize: 13 }}>
|
|
2501
|
+
🔒 {tr("docker.ports.mainFixed", ":8008 · cicy-code(固定)")}
|
|
2502
|
+
</div>
|
|
2503
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8, maxHeight: 220, overflowY: "auto" }}>
|
|
2504
|
+
{portList.length === 0 && (
|
|
2505
|
+
<div style={{ fontSize: 12.5, color: "#6e7681", padding: "4px 2px" }}>{tr("docker.ports.none", "暂无额外端口")}</div>
|
|
2506
|
+
)}
|
|
2507
|
+
{portList.map((p, i) => (
|
|
2508
|
+
<div key={i} style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
2509
|
+
<input data-id={`DockerCard-port-input-${i}`} type="text" inputMode="numeric" value={p}
|
|
2510
|
+
placeholder={tr("docker.ports.ph", "端口号 1-65535")}
|
|
2511
|
+
onChange={(e) => { const v = e.target.value.replace(/[^0-9]/g, ""); setPortList((list) => list.map((x, j) => j === i ? v : x)); setPortsErr(""); }}
|
|
2512
|
+
style={{ flex: 1, padding: "7px 10px", borderRadius: 8, border: "1px solid #30363d", background: "#0d1117", color: "#e6edf3", fontSize: 13, fontFamily: "var(--mono)" }} />
|
|
2513
|
+
<button type="button" data-id={`DockerCard-port-remove-${i}`} title={tr("docker.ports.remove", "移除")}
|
|
2514
|
+
onClick={() => { setPortList((list) => list.filter((_, j) => j !== i)); setPortsErr(""); }}
|
|
2515
|
+
style={{ padding: "6px 10px", borderRadius: 8, border: "1px solid #30363d", background: "transparent", color: "#da3633", cursor: "pointer", fontSize: 14 }}>✕</button>
|
|
2516
|
+
</div>
|
|
2517
|
+
))}
|
|
2518
|
+
</div>
|
|
2519
|
+
<button type="button" data-id="DockerCard-port-add"
|
|
2520
|
+
onClick={() => setPortList((list) => [...list, ""])}
|
|
2521
|
+
style={{ marginTop: 10, padding: "7px 12px", borderRadius: 8, border: "1px dashed #30363d", background: "transparent", color: "#58a6ff", cursor: "pointer", fontSize: 13, width: "100%" }}>
|
|
2522
|
+
{tr("docker.ports.add", "+ 添加端口")}
|
|
2523
|
+
</button>
|
|
2524
|
+
{portsErr && <div style={{ marginTop: 10, fontSize: 12.5, color: "#f85149" }}>{portsErr}</div>}
|
|
2525
|
+
<p style={{ margin: "12px 0 16px", fontSize: 11.5, lineHeight: 1.5, color: "#6e7681" }}>{tr("docker.ports.hint", "保存会重建容器(volume 数据保留),会有短暂中断")}</p>
|
|
2526
|
+
<div style={{ display: "flex", justifyContent: "flex-end", gap: 10 }}>
|
|
2527
|
+
<button type="button" data-id="DockerCard-ports-cancel" disabled={portsBusy}
|
|
2528
|
+
onClick={() => setPortsOpen(false)}
|
|
2529
|
+
style={{ padding: "7px 16px", borderRadius: 8, border: "1px solid #30363d", background: "transparent", color: "#c9d1d9", cursor: "pointer", fontSize: 13 }}>
|
|
2530
|
+
{tr("docker.ports.cancel", "取消")}
|
|
2531
|
+
</button>
|
|
2532
|
+
<button type="button" data-id="DockerCard-ports-save" disabled={portsBusy}
|
|
2533
|
+
onClick={savePorts}
|
|
2534
|
+
style={{ padding: "7px 16px", borderRadius: 8, border: "none", background: "#238636", color: "white", cursor: "pointer", fontSize: 13, fontWeight: 600 }}>
|
|
2535
|
+
{portsBusy ? tr("docker.ports.saving", "保存中…") : tr("docker.ports.save", "保存并重建")}
|
|
2536
|
+
</button>
|
|
2537
|
+
</div>
|
|
2538
|
+
</div>
|
|
2539
|
+
</div>,
|
|
2540
|
+
document.body
|
|
2541
|
+
)}
|
|
2411
2542
|
</div>
|
|
2412
2543
|
);
|
|
2413
2544
|
}
|