@co0ontty/wand 1.34.0 → 1.35.1

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/dist/server.js CHANGED
@@ -86,11 +86,33 @@ function compareSemver(a, b) {
86
86
  return -1;
87
87
  if (!pa.pre && !pb.pre)
88
88
  return 0;
89
- // Both have prerelease: lexical compare handles debug.MMDDHHMM ordering.
90
- if (pa.pre < pb.pre)
91
- return -1;
92
- if (pa.pre > pb.pre)
93
- return 1;
89
+ // Both have prerelease: . 分段比较 (数字段数值比, 非数字段字典序), 贴近标准 semver,
90
+ // 避免跨月/跨年的 debug.MMDDHHMM 后缀因纯字典序而排反。
91
+ const segA = pa.pre.split(".");
92
+ const segB = pb.pre.split(".");
93
+ const segLen = Math.max(segA.length, segB.length);
94
+ for (let i = 0; i < segLen; i++) {
95
+ const sa = segA[i];
96
+ const sb = segB[i];
97
+ if (sa === undefined)
98
+ return -1; // 段少者更小
99
+ if (sb === undefined)
100
+ return 1;
101
+ const na = Number(sa);
102
+ const nb = Number(sb);
103
+ const aIsNum = sa !== "" && !Number.isNaN(na);
104
+ const bIsNum = sb !== "" && !Number.isNaN(nb);
105
+ if (aIsNum && bIsNum) {
106
+ if (na !== nb)
107
+ return na < nb ? -1 : 1;
108
+ }
109
+ else if (aIsNum !== bIsNum) {
110
+ return aIsNum ? -1 : 1; // 数字段 < 非数字段
111
+ }
112
+ else if (sa !== sb) {
113
+ return sa < sb ? -1 : 1;
114
+ }
115
+ }
94
116
  return 0;
95
117
  }
96
118
  let cachedGitHubApk = null;
@@ -119,6 +141,7 @@ async function fetchGitHubLatestApk(forceRefresh = false) {
119
141
  downloadUrl: apkAsset.browser_download_url,
120
142
  fileName: apkAsset.name,
121
143
  size: apkAsset.size,
144
+ releaseNotes: release.body ? release.body.trim().slice(0, 500) : undefined,
122
145
  };
123
146
  gitHubApkCacheTs = now;
124
147
  return cachedGitHubApk;
@@ -148,6 +171,7 @@ async function resolveLatestApkVersion(configDir, config) {
148
171
  fileName: ghApk.fileName,
149
172
  size: ghApk.size,
150
173
  source: "github",
174
+ releaseNotes: ghApk.releaseNotes,
151
175
  };
152
176
  }
153
177
  return null;
@@ -461,7 +485,24 @@ async function resolveAndroidApkAsset(configDir, config) {
461
485
  fileStat,
462
486
  };
463
487
  }));
464
- candidates.sort((a, b) => b.fileStat.mtimeMs - a.fileStat.mtimeMs);
488
+ // 按语义版本选"最新", 而非修改时间 —— cp/rsync/解压/checkout 都可能让低版本号文件的
489
+ // mtime 更新, 用 mtime 会把旧版本号当成 latest 上报。版本相同或都无版本号时退回 mtime。
490
+ candidates.sort((a, b) => {
491
+ const va = extractAndroidApkVersion(a.entry.name);
492
+ const vb = extractAndroidApkVersion(b.entry.name);
493
+ if (va && vb) {
494
+ const cmp = compareSemver(vb, va);
495
+ if (cmp !== 0)
496
+ return cmp;
497
+ }
498
+ else if (va && !vb) {
499
+ return -1;
500
+ }
501
+ else if (!va && vb) {
502
+ return 1;
503
+ }
504
+ return b.fileStat.mtimeMs - a.fileStat.mtimeMs;
505
+ });
465
506
  const selected = candidates[0];
466
507
  return {
467
508
  fileName: selected.entry.name,
@@ -936,22 +977,66 @@ export async function startServer(config, configPath) {
936
977
  fileName: updateAvailable ? latest.fileName : null,
937
978
  size: updateAvailable ? latest.size : null,
938
979
  source: latest.source,
980
+ releaseNotes: updateAvailable ? (latest.releaseNotes ?? null) : null,
939
981
  });
940
982
  });
941
- app.get("/android/download", async (_req, res) => {
942
- const androidApk = await resolveAndroidApkAsset(configDir, config);
983
+ app.get("/android/download", async (req, res) => {
943
984
  if (config.android?.enabled !== true) {
944
985
  res.status(404).json({ error: "Android APK 下载未启用。" });
945
986
  return;
946
987
  }
988
+ const androidApk = await resolveAndroidApkAsset(configDir, config);
947
989
  if (!androidApk) {
948
990
  res.status(404).json({ error: "当前没有可下载的 APK 文件。" });
949
991
  return;
950
992
  }
993
+ const total = androidApk.size;
951
994
  res.setHeader("Content-Type", "application/vnd.android.package-archive");
952
- res.setHeader("Content-Length", String(androidApk.size));
953
995
  res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(androidApk.fileName)}"`);
954
- createReadStream(androidApk.filePath).pipe(res);
996
+ // 声明支持断点续传 — 弱网/移动网络下中断后客户端可带 Range 续传, 而非从头重下。
997
+ res.setHeader("Accept-Ranges", "bytes");
998
+ // 解析 Range: bytes=start-end (含后缀范围 bytes=-N)。命中则返回 206 + 局部内容。
999
+ let start = 0;
1000
+ let end = total - 1;
1001
+ let isPartial = false;
1002
+ const rangeHeader = req.headers.range;
1003
+ if (rangeHeader) {
1004
+ const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
1005
+ if (match && (match[1] !== "" || match[2] !== "")) {
1006
+ if (match[1] === "") {
1007
+ // bytes=-N: 最后 N 字节
1008
+ start = Math.max(0, total - Number(match[2]));
1009
+ end = total - 1;
1010
+ }
1011
+ else {
1012
+ start = Number(match[1]);
1013
+ end = match[2] === "" ? total - 1 : Math.min(Number(match[2]), total - 1);
1014
+ }
1015
+ isPartial = true;
1016
+ if (start > end || start < 0 || start >= total) {
1017
+ res.status(416);
1018
+ res.setHeader("Content-Range", `bytes */${total}`);
1019
+ res.end();
1020
+ return;
1021
+ }
1022
+ }
1023
+ }
1024
+ if (isPartial) {
1025
+ res.status(206);
1026
+ res.setHeader("Content-Range", `bytes ${start}-${end}/${total}`);
1027
+ }
1028
+ res.setHeader("Content-Length", String(end - start + 1));
1029
+ const stream = createReadStream(androidApk.filePath, { start, end });
1030
+ stream.on("error", (err) => {
1031
+ // 文件在 stat 之后被删/读错误时, 让客户端尽快感知断流, 而不是等满读超时。
1032
+ if (!res.headersSent) {
1033
+ res.status(500).json({ error: getErrorMessage(err, "读取 APK 文件失败。") });
1034
+ }
1035
+ else {
1036
+ res.destroy();
1037
+ }
1038
+ });
1039
+ stream.pipe(res);
955
1040
  });
956
1041
  // ── macOS DMG update & download (no auth required) ──
957
1042
  app.get("/api/macos-dmg-update", async (req, res) => {
@@ -2915,7 +2915,7 @@
2915
2915
  '<div id="android-auto-update-row" class="settings-toggle-row hidden">' +
2916
2916
  '<div class="settings-toggle-text">' +
2917
2917
  '<span class="settings-toggle-title">自动更新</span>' +
2918
- '<span class="settings-toggle-desc" id="android-auto-update-hint">检测到新版 APK 将自动下载安装。</span>' +
2918
+ '<span class="settings-toggle-desc" id="android-auto-update-hint">检测到新版 APK 时自动拉起下载,安装仍需在系统中确认。</span>' +
2919
2919
  '</div>' +
2920
2920
  '<label class="settings-switch">' +
2921
2921
  '<input type="checkbox" id="auto-update-apk-toggle" class="switch-toggle">' +
@@ -10301,9 +10301,81 @@
10301
10301
  var autoUpdateWebToggle = document.getElementById("auto-update-web-toggle");
10302
10302
  if (autoUpdateWebToggle) autoUpdateWebToggle.checked = !!autoUpdate.web;
10303
10303
  var autoUpdateApkToggle = document.getElementById("auto-update-apk-toggle");
10304
- if (autoUpdateApkToggle) autoUpdateApkToggle.checked = !!autoUpdate.apk;
10304
+ // 自动更新开关只对 APK 壳生效, 浏览器里不绑定状态(该行也保持隐藏), 避免静默写一个看不见的控件。
10305
+ if (autoUpdateApkToggle) autoUpdateApkToggle.checked = !!_apkVersion && !!autoUpdate.apk;
10305
10306
  var autoUpdateDmgToggle = document.getElementById("auto-update-dmg-toggle");
10306
- if (autoUpdateDmgToggle) autoUpdateDmgToggle.checked = !!autoUpdate.dmg;
10307
+ if (autoUpdateDmgToggle) autoUpdateDmgToggle.checked = !!_macAppVersion && !!autoUpdate.dmg;
10308
+
10309
+ // ── 原生包下载 helper(APK / DMG 共用)──
10310
+ function safeNotify(msg, type) {
10311
+ if (typeof window.wandAlert === "function") {
10312
+ window.wandAlert(msg, { type: type === "error" ? "danger" : "info" });
10313
+ } else if (typeof showToast === "function") {
10314
+ showToast(msg, type === "error" ? "error" : "info");
10315
+ } else if (type === "error") {
10316
+ alert(msg);
10317
+ }
10318
+ }
10319
+ // 同源本地下载:先 HEAD 探测状态码, 避免 window.open("_self") 把整页导航成裸 JSON;
10320
+ // 命中则用隐藏 <a download> 触发下载, 并给出明确反馈。
10321
+ function triggerLocalDownload(url, fileName, btn) {
10322
+ var original = btn ? btn.textContent : "";
10323
+ if (btn) { btn.disabled = true; btn.textContent = "下载中…"; }
10324
+ function restore() { if (btn) { btn.disabled = false; btn.textContent = original; } }
10325
+ fetch(url, { method: "HEAD", credentials: "same-origin" })
10326
+ .then(function(resp) {
10327
+ if (!resp.ok) {
10328
+ safeNotify(resp.status === 404 ? "下载未启用或文件已移除" : ("下载失败 (HTTP " + resp.status + ")"), "error");
10329
+ restore();
10330
+ return;
10331
+ }
10332
+ var a = document.createElement("a");
10333
+ a.href = url;
10334
+ a.download = fileName || "";
10335
+ document.body.appendChild(a);
10336
+ a.click();
10337
+ document.body.removeChild(a);
10338
+ safeNotify("已开始下载,请在通知栏/下载管理中查看", "info");
10339
+ restore();
10340
+ })
10341
+ .catch(function(err) {
10342
+ safeNotify("下载失败: " + (err && err.message ? err.message : err), "error");
10343
+ restore();
10344
+ });
10345
+ }
10346
+
10347
+ // 设置页版本比较 (仅主版本三段, 预发布忽略) — 判断该升级 / 已最新 / 更旧。
10348
+ function compareVer(a, b) {
10349
+ function parse(v) {
10350
+ return String(v || "").replace(/^v/, "").split("-")[0].split(".").map(function(n) { return parseInt(n, 10) || 0; });
10351
+ }
10352
+ var pa = parse(a), pb = parse(b);
10353
+ for (var i = 0; i < 3; i++) {
10354
+ var d = (pa[i] || 0) - (pb[i] || 0);
10355
+ if (d !== 0) return d > 0 ? 1 : -1;
10356
+ }
10357
+ return 0;
10358
+ }
10359
+ // 壳内按钮: 据版本比较结果决定文案/可点性, 避免线上比已装旧时仍诱导"下载安装"。
10360
+ function applyApkButton(btn, cmp, url, fileName, source) {
10361
+ btn.classList.remove("hidden");
10362
+ btn.disabled = false;
10363
+ if (cmp > 0) {
10364
+ btn.textContent = "升级";
10365
+ } else if (cmp === 0) {
10366
+ btn.textContent = "已是最新";
10367
+ btn.disabled = true;
10368
+ } else {
10369
+ btn.textContent = "重新安装";
10370
+ }
10371
+ btn.onclick = btn.disabled ? null : function() {
10372
+ try {
10373
+ WandNative.downloadUpdate(url, fileName, source);
10374
+ } catch (e) {
10375
+ safeNotify("调用下载失败: " + (e && e.message ? e.message : e), "error");
10376
+ }
10377
+ };
10378
+ }
10307
10379
 
10308
10380
  // ── Android APK version display ──
10309
10381
  var apkSection = document.getElementById("android-apk-section");
@@ -10318,7 +10390,9 @@
10318
10390
  var apkMessageEl = document.getElementById("android-apk-message");
10319
10391
  var androidApk = data.androidApk || {};
10320
10392
  var isInApk = !!_apkVersion;
10321
- var hasApkInfo = isInApk || !!androidApk.github || !!androidApk.local;
10393
+ // 浏览器模式下若管理员未启用 Android 分发(enabled===false), 整段隐藏; 壳内保留以便自升级。
10394
+ var apkEnabled = androidApk.enabled !== false;
10395
+ var hasApkInfo = isInApk || (apkEnabled && (!!androidApk.github || !!androidApk.local));
10322
10396
  if (apkSection) {
10323
10397
  if (hasApkInfo) apkSection.classList.remove("hidden");
10324
10398
  else apkSection.classList.add("hidden");
@@ -10337,21 +10411,8 @@
10337
10411
  apkGithubEl.textContent = ghLabel;
10338
10412
  apkGithubRow.classList.remove("hidden");
10339
10413
  if (apkGithubBtn) {
10340
- apkGithubBtn.textContent = "下载安装";
10341
- apkGithubBtn.classList.remove("hidden");
10342
- apkGithubBtn.onclick = function() {
10343
- try {
10344
- WandNative.downloadUpdate(androidApk.github.downloadUrl, androidApk.github.fileName || "wand-update.apk", "github");
10345
- } catch (e) {
10346
- if (typeof window.wandAlert === "function") {
10347
- window.wandAlert("调用下载失败: " + e.message, { type: "danger", title: "下载失败" });
10348
- } else if (typeof showToast === "function") {
10349
- showToast("调用下载失败: " + e.message, "error");
10350
- } else {
10351
- alert("调用下载失败: " + e.message);
10352
- }
10353
- }
10354
- };
10414
+ var ghCmp = androidApk.github.version ? compareVer(androidApk.github.version, _apkVersion) : 1;
10415
+ applyApkButton(apkGithubBtn, ghCmp, androidApk.github.downloadUrl, androidApk.github.fileName || "wand-update.apk", "github");
10355
10416
  }
10356
10417
  }
10357
10418
  // 本地版本
@@ -10361,21 +10422,8 @@
10361
10422
  apkLocalEl.textContent = lcLabel;
10362
10423
  apkLocalRow.classList.remove("hidden");
10363
10424
  if (apkLocalBtn) {
10364
- apkLocalBtn.textContent = "下载安装";
10365
- apkLocalBtn.classList.remove("hidden");
10366
- apkLocalBtn.onclick = function() {
10367
- try {
10368
- WandNative.downloadUpdate(androidApk.local.downloadUrl, androidApk.local.fileName || "wand-update.apk", "local");
10369
- } catch (e) {
10370
- if (typeof window.wandAlert === "function") {
10371
- window.wandAlert("调用下载失败: " + e.message, { type: "danger", title: "下载失败" });
10372
- } else if (typeof showToast === "function") {
10373
- showToast("调用下载失败: " + e.message, "error");
10374
- } else {
10375
- alert("调用下载失败: " + e.message);
10376
- }
10377
- }
10378
- };
10425
+ var lcCmp = androidApk.local.version ? compareVer(androidApk.local.version, _apkVersion) : 1;
10426
+ applyApkButton(apkLocalBtn, lcCmp, androidApk.local.downloadUrl, androidApk.local.fileName || "wand-update.apk", "local");
10379
10427
  }
10380
10428
  }
10381
10429
  // 都没有时
@@ -10400,6 +10448,7 @@
10400
10448
  apkGithubBtn.classList.remove("hidden");
10401
10449
  apkGithubBtn.onclick = function() {
10402
10450
  window.open(androidApk.github.downloadUrl, "_blank");
10451
+ safeNotify("正在打开下载页…", "info");
10403
10452
  };
10404
10453
  }
10405
10454
  }
@@ -10413,12 +10462,12 @@
10413
10462
  apkLocalBtn.textContent = "下载";
10414
10463
  apkLocalBtn.classList.remove("hidden");
10415
10464
  apkLocalBtn.onclick = function() {
10416
- window.open(androidApk.local.downloadUrl, "_self");
10465
+ triggerLocalDownload(androidApk.local.downloadUrl, androidApk.local.fileName || "wand-update.apk", apkLocalBtn);
10417
10466
  };
10418
10467
  }
10419
10468
  }
10420
10469
  if (!androidApk.github && !androidApk.local && apkMessageEl) {
10421
- apkMessageEl.textContent = "暂未提供";
10470
+ apkMessageEl.textContent = apkEnabled ? "在线版本暂时获取失败,可稍后重试" : "Android 下载未在服务端启用";
10422
10471
  apkMessageEl.classList.remove("hidden");
10423
10472
  }
10424
10473
  }
@@ -10436,7 +10485,8 @@
10436
10485
  var dmgMessageEl = document.getElementById("macos-dmg-message");
10437
10486
  var macosDmg = data.macosDmg || {};
10438
10487
  var isInMacApp = !!_macAppVersion;
10439
- var hasDmgInfo = isInMacApp || !!macosDmg.github || !!macosDmg.local;
10488
+ var dmgEnabled = macosDmg.enabled !== false;
10489
+ var hasDmgInfo = isInMacApp || (dmgEnabled && (!!macosDmg.github || !!macosDmg.local));
10440
10490
  if (dmgSection) {
10441
10491
  if (hasDmgInfo) dmgSection.classList.remove("hidden");
10442
10492
  else dmgSection.classList.add("hidden");
@@ -10454,21 +10504,8 @@
10454
10504
  dmgGithubEl.textContent = dghLabel;
10455
10505
  dmgGithubRow.classList.remove("hidden");
10456
10506
  if (dmgGithubBtn) {
10457
- dmgGithubBtn.textContent = "下载安装";
10458
- dmgGithubBtn.classList.remove("hidden");
10459
- dmgGithubBtn.onclick = function() {
10460
- try {
10461
- WandNative.downloadUpdate(macosDmg.github.downloadUrl, macosDmg.github.fileName || "wand-update.dmg", "github");
10462
- } catch (e) {
10463
- if (typeof window.wandAlert === "function") {
10464
- window.wandAlert("调用下载失败: " + e.message, { type: "danger", title: "下载失败" });
10465
- } else if (typeof showToast === "function") {
10466
- showToast("调用下载失败: " + e.message, "error");
10467
- } else {
10468
- alert("调用下载失败: " + e.message);
10469
- }
10470
- }
10471
- };
10507
+ var dghCmp = macosDmg.github.version ? compareVer(macosDmg.github.version, _macAppVersion) : 1;
10508
+ applyApkButton(dmgGithubBtn, dghCmp, macosDmg.github.downloadUrl, macosDmg.github.fileName || "wand-update.dmg", "github");
10472
10509
  }
10473
10510
  }
10474
10511
  if (macosDmg.local && dmgLocalRow && dmgLocalEl) {
@@ -10477,21 +10514,8 @@
10477
10514
  dmgLocalEl.textContent = dlcLabel;
10478
10515
  dmgLocalRow.classList.remove("hidden");
10479
10516
  if (dmgLocalBtn) {
10480
- dmgLocalBtn.textContent = "下载安装";
10481
- dmgLocalBtn.classList.remove("hidden");
10482
- dmgLocalBtn.onclick = function() {
10483
- try {
10484
- WandNative.downloadUpdate(macosDmg.local.downloadUrl, macosDmg.local.fileName || "wand-update.dmg", "local");
10485
- } catch (e) {
10486
- if (typeof window.wandAlert === "function") {
10487
- window.wandAlert("调用下载失败: " + e.message, { type: "danger", title: "下载失败" });
10488
- } else if (typeof showToast === "function") {
10489
- showToast("调用下载失败: " + e.message, "error");
10490
- } else {
10491
- alert("调用下载失败: " + e.message);
10492
- }
10493
- }
10494
- };
10517
+ var dlcCmp = macosDmg.local.version ? compareVer(macosDmg.local.version, _macAppVersion) : 1;
10518
+ applyApkButton(dmgLocalBtn, dlcCmp, macosDmg.local.downloadUrl, macosDmg.local.fileName || "wand-update.dmg", "local");
10495
10519
  }
10496
10520
  }
10497
10521
  if (!macosDmg.github && !macosDmg.local && dmgMessageEl) {
@@ -10514,6 +10538,7 @@
10514
10538
  dmgGithubBtn.classList.remove("hidden");
10515
10539
  dmgGithubBtn.onclick = function() {
10516
10540
  window.open(macosDmg.github.downloadUrl, "_blank");
10541
+ safeNotify("正在打开下载页…", "info");
10517
10542
  };
10518
10543
  }
10519
10544
  }
@@ -10526,12 +10551,12 @@
10526
10551
  dmgLocalBtn.textContent = "下载";
10527
10552
  dmgLocalBtn.classList.remove("hidden");
10528
10553
  dmgLocalBtn.onclick = function() {
10529
- window.open(macosDmg.local.downloadUrl, "_self");
10554
+ triggerLocalDownload(macosDmg.local.downloadUrl, macosDmg.local.fileName || "wand-update.dmg", dmgLocalBtn);
10530
10555
  };
10531
10556
  }
10532
10557
  }
10533
10558
  if (!macosDmg.github && !macosDmg.local && dmgMessageEl) {
10534
- dmgMessageEl.textContent = "暂未提供";
10559
+ dmgMessageEl.textContent = dmgEnabled ? "在线版本暂时获取失败,可稍后重试" : "macOS 下载未在服务端启用";
10535
10560
  dmgMessageEl.classList.remove("hidden");
10536
10561
  }
10537
10562
  }
@@ -42,7 +42,7 @@ export function renderApp(configPath) {
42
42
  <html lang="zh-CN">
43
43
  <head>
44
44
  <meta charset="utf-8" />
45
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
45
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content" />
46
46
  <title>Wand Console</title>
47
47
  <meta name="description" content="Local CLI Console for Vibe Coding - Manage terminal sessions from your browser" />
48
48
  <meta name="theme-color" content="#f6f1e8" media="(prefers-color-scheme: light)" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.34.0",
3
+ "version": "1.35.1",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {