@co0ontty/wand 1.33.0 → 1.35.0

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) => {
@@ -1390,7 +1390,7 @@
1390
1390
  checkDmgAutoUpdate();
1391
1391
  }
1392
1392
  if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
1393
- loadClaudeHistory();
1393
+ ensureClaudeHistoryLoaded();
1394
1394
  }
1395
1395
  });
1396
1396
  })
@@ -1462,8 +1462,11 @@
1462
1462
  // Suppress CSS transitions during initial DOM build
1463
1463
  document.documentElement.classList.add("no-transition");
1464
1464
 
1465
- // Apply persisted pin state before rendering
1466
- if (state.sidebarPinned && !isMobileLayout()) {
1465
+ // Apply persisted pin state before rendering.
1466
+ // 窄条(collapsed)形态不靠 .open 显示,靠 .pinned.collapsed 的 width:56px
1467
+ // 常驻;此时强制 sessionsDrawerOpen=true 会与 toggleSidebarCollapsed 里设的
1468
+ // false 打架,并在手机端误触发背景遮罩。窄条态下不强制 open。
1469
+ if (state.sidebarPinned && !state.sidebarCollapsed && !isMobileLayout()) {
1467
1470
  state.sessionsDrawerOpen = true;
1468
1471
  }
1469
1472
  app.innerHTML = isLoggedIn ? renderAppShell() : renderLogin();
@@ -2912,7 +2915,7 @@
2912
2915
  '<div id="android-auto-update-row" class="settings-toggle-row hidden">' +
2913
2916
  '<div class="settings-toggle-text">' +
2914
2917
  '<span class="settings-toggle-title">自动更新</span>' +
2915
- '<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>' +
2916
2919
  '</div>' +
2917
2920
  '<label class="settings-switch">' +
2918
2921
  '<input type="checkbox" id="auto-update-apk-toggle" class="switch-toggle">' +
@@ -3558,6 +3561,11 @@
3558
3561
 
3559
3562
  function toggleManageMode(force) {
3560
3563
  state.sessionsManageMode = typeof force === "boolean" ? force : !state.sessionsManageMode;
3564
+ if (state.sessionsManageMode && !state.claudeHistoryLoaded) {
3565
+ // 进入管理模式即后台补齐 Claude 历史,让「已选 N」「全选」计数从一开始
3566
+ // 就覆盖全部历史,而不是只统计已加载的那部分。
3567
+ ensureClaudeHistoryLoaded().then(updateSessionsList);
3568
+ }
3561
3569
  if (!state.sessionsManageMode) {
3562
3570
  clearManageSelections();
3563
3571
  closeSwipedItem();
@@ -3574,6 +3582,16 @@
3574
3582
  }
3575
3583
 
3576
3584
  function selectAllVisibleItems() {
3585
+ // 全选语义 = 选中所有可管理项(会话 + 全部 Claude 历史)。历史在登录后
3586
+ // 异步扫描,若用户在扫描完成前点「全选」,state.claudeHistory 仍为空会漏选,
3587
+ // 删除时表现为"只删了已加载的,跨目录/未扫完的历史还在"。这里先确保历史
3588
+ // 加载完成再全选。
3589
+ if (!state.claudeHistoryLoaded) {
3590
+ ensureClaudeHistoryLoaded().then(selectAllVisibleItems);
3591
+ return;
3592
+ }
3593
+ // 展开 Claude 历史分组,让用户能直观看到这些历史项也被选中了。
3594
+ state.claudeHistoryExpanded = true;
3577
3595
  var nextSessionIds = {};
3578
3596
  getSelectableSessions().forEach(function(session) {
3579
3597
  nextSessionIds[session.id] = true;
@@ -3782,6 +3800,21 @@
3782
3800
  });
3783
3801
  }
3784
3802
 
3803
+ // 去重包装:登录后历史会异步扫描,多个入口(管理模式、全选、展开分组)
3804
+ // 可能同时想确保历史就绪。共享同一个 in-flight Promise,避免重复 fetch,
3805
+ // 且在已加载时立即 resolve。
3806
+ var _claudeHistoryLoadingPromise = null;
3807
+ function ensureClaudeHistoryLoaded() {
3808
+ if (state.claudeHistoryLoaded) return Promise.resolve();
3809
+ if (_claudeHistoryLoadingPromise) return _claudeHistoryLoadingPromise;
3810
+ _claudeHistoryLoadingPromise = loadClaudeHistory().then(function() {
3811
+ _claudeHistoryLoadingPromise = null;
3812
+ }, function() {
3813
+ _claudeHistoryLoadingPromise = null;
3814
+ });
3815
+ return _claudeHistoryLoadingPromise;
3816
+ }
3817
+
3785
3818
  function isMobileLayout() {
3786
3819
  return window.innerWidth <= 768;
3787
3820
  }
@@ -5892,11 +5925,17 @@
5892
5925
  if (sidebarMoreBtn && sidebarOverflow) {
5893
5926
  sidebarMoreBtn.addEventListener("click", function(e) {
5894
5927
  e.stopPropagation();
5895
- sidebarOverflow.classList.toggle("open");
5928
+ var willOpen = !sidebarOverflow.classList.contains("open");
5929
+ sidebarOverflow.classList.toggle("open", willOpen);
5930
+ if (willOpen) positionSidebarOverflowMenu(sidebarOverflow);
5896
5931
  });
5897
5932
  document.addEventListener("click", function() {
5898
5933
  sidebarOverflow.classList.remove("open");
5899
5934
  });
5935
+ // 视口尺寸变化时关闭,避免 clamp 后的定位与新尺寸不符。
5936
+ window.addEventListener("resize", function() {
5937
+ sidebarOverflow.classList.remove("open");
5938
+ });
5900
5939
  }
5901
5940
  var homeBtn = document.getElementById("sidebar-home-btn");
5902
5941
  if (homeBtn) homeBtn.addEventListener("click", function() {
@@ -6964,7 +7003,7 @@
6964
7003
  event.stopPropagation();
6965
7004
  state.claudeHistoryExpanded = !state.claudeHistoryExpanded;
6966
7005
  if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
6967
- loadClaudeHistory();
7006
+ ensureClaudeHistoryLoaded();
6968
7007
  }
6969
7008
  updateSessionsList();
6970
7009
  return;
@@ -9588,10 +9627,55 @@
9588
9627
  // 桌面端展开窄条 → 300px 全栏常驻。
9589
9628
  state.sessionsDrawerOpen = true;
9590
9629
  }
9591
- render();
9630
+ // 轻量更新而非全量 render():render() 会 teardown 并重建整个终端 DOM,
9631
+ // 导致收窄/展开时终端闪烁、丢失滚动与渲染状态。这里只切布局 class
9632
+ // (宽度 56↔300 走 CSS width transition)、重渲侧栏列表内容、刷新
9633
+ // 收窄按钮自身的图标/文案,终端区保持不动。
9634
+ updateLayoutState();
9635
+ updateSessionsList();
9636
+ updateSidebarCollapseButton();
9637
+ hideCollapsedTileBubble();
9592
9638
  scheduleTerminalRefitAfterPaddingTransition();
9593
9639
  }
9594
9640
 
9641
+ // 收窄按钮的图标/title/状态随 collapsed 切换。抽出来给轻量更新路径用,
9642
+ // 避免为了换一个箭头方向就走全量 render()。
9643
+ function updateSidebarCollapseButton() {
9644
+ var btn = document.getElementById("sidebar-collapse-btn");
9645
+ if (!btn) return;
9646
+ var isCollapsed = !!state.sidebarPinned && !!state.sidebarCollapsed;
9647
+ btn.classList.toggle("collapsed", isCollapsed);
9648
+ btn.title = isCollapsed ? "展开侧栏" : "收起为窄条";
9649
+ btn.setAttribute("aria-label", isCollapsed ? "展开侧栏" : "收起为窄条");
9650
+ btn.innerHTML = isCollapsed
9651
+ ? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="10 6 16 12 10 18"/><line x1="20" y1="5" x2="20" y2="19"/></svg>'
9652
+ : '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="14 6 8 12 14 18"/><line x1="4" y1="5" x2="4" y2="19"/></svg>';
9653
+ }
9654
+
9655
+ // 「更多操作」下拉默认 right:0 贴 more 按钮右沿向左展开。手机窄屏下这条会把
9656
+ // 菜单左缘顶出屏幕外。打开时按视口边界 clamp:先保持 CSS 默认右对齐,仅当
9657
+ // 真的越界才改写 left/right 把菜单拉回视口内(留 8px 边距)。
9658
+ function positionSidebarOverflowMenu(menu) {
9659
+ if (!menu) return;
9660
+ menu.style.left = "";
9661
+ menu.style.right = "";
9662
+ var parent = menu.offsetParent || menu.parentElement;
9663
+ if (!parent) return;
9664
+ var margin = 8;
9665
+ var parentRect = parent.getBoundingClientRect();
9666
+ var rect = menu.getBoundingClientRect();
9667
+ var vw = window.innerWidth;
9668
+ if (rect.left < margin) {
9669
+ // 左缘越界:改用 left 定位,把左缘顶到视口左 margin。
9670
+ menu.style.right = "auto";
9671
+ menu.style.left = (margin - parentRect.left) + "px";
9672
+ } else if (rect.right > vw - margin) {
9673
+ // 右缘越界:拉回视口右 margin(桌面右对齐时几乎不会触发)。
9674
+ menu.style.left = "auto";
9675
+ menu.style.right = (parentRect.right - (vw - margin)) + "px";
9676
+ }
9677
+ }
9678
+
9595
9679
 
9596
9680
  // Store last focused element for focus trap
9597
9681
  var lastFocusedElement = null;
@@ -10217,9 +10301,81 @@
10217
10301
  var autoUpdateWebToggle = document.getElementById("auto-update-web-toggle");
10218
10302
  if (autoUpdateWebToggle) autoUpdateWebToggle.checked = !!autoUpdate.web;
10219
10303
  var autoUpdateApkToggle = document.getElementById("auto-update-apk-toggle");
10220
- if (autoUpdateApkToggle) autoUpdateApkToggle.checked = !!autoUpdate.apk;
10304
+ // 自动更新开关只对 APK 壳生效, 浏览器里不绑定状态(该行也保持隐藏), 避免静默写一个看不见的控件。
10305
+ if (autoUpdateApkToggle) autoUpdateApkToggle.checked = !!_apkVersion && !!autoUpdate.apk;
10221
10306
  var autoUpdateDmgToggle = document.getElementById("auto-update-dmg-toggle");
10222
- 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
+ }
10223
10379
 
10224
10380
  // ── Android APK version display ──
10225
10381
  var apkSection = document.getElementById("android-apk-section");
@@ -10234,7 +10390,9 @@
10234
10390
  var apkMessageEl = document.getElementById("android-apk-message");
10235
10391
  var androidApk = data.androidApk || {};
10236
10392
  var isInApk = !!_apkVersion;
10237
- 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));
10238
10396
  if (apkSection) {
10239
10397
  if (hasApkInfo) apkSection.classList.remove("hidden");
10240
10398
  else apkSection.classList.add("hidden");
@@ -10253,21 +10411,8 @@
10253
10411
  apkGithubEl.textContent = ghLabel;
10254
10412
  apkGithubRow.classList.remove("hidden");
10255
10413
  if (apkGithubBtn) {
10256
- apkGithubBtn.textContent = "下载安装";
10257
- apkGithubBtn.classList.remove("hidden");
10258
- apkGithubBtn.onclick = function() {
10259
- try {
10260
- WandNative.downloadUpdate(androidApk.github.downloadUrl, androidApk.github.fileName || "wand-update.apk", "github");
10261
- } catch (e) {
10262
- if (typeof window.wandAlert === "function") {
10263
- window.wandAlert("调用下载失败: " + e.message, { type: "danger", title: "下载失败" });
10264
- } else if (typeof showToast === "function") {
10265
- showToast("调用下载失败: " + e.message, "error");
10266
- } else {
10267
- alert("调用下载失败: " + e.message);
10268
- }
10269
- }
10270
- };
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");
10271
10416
  }
10272
10417
  }
10273
10418
  // 本地版本
@@ -10277,21 +10422,8 @@
10277
10422
  apkLocalEl.textContent = lcLabel;
10278
10423
  apkLocalRow.classList.remove("hidden");
10279
10424
  if (apkLocalBtn) {
10280
- apkLocalBtn.textContent = "下载安装";
10281
- apkLocalBtn.classList.remove("hidden");
10282
- apkLocalBtn.onclick = function() {
10283
- try {
10284
- WandNative.downloadUpdate(androidApk.local.downloadUrl, androidApk.local.fileName || "wand-update.apk", "local");
10285
- } catch (e) {
10286
- if (typeof window.wandAlert === "function") {
10287
- window.wandAlert("调用下载失败: " + e.message, { type: "danger", title: "下载失败" });
10288
- } else if (typeof showToast === "function") {
10289
- showToast("调用下载失败: " + e.message, "error");
10290
- } else {
10291
- alert("调用下载失败: " + e.message);
10292
- }
10293
- }
10294
- };
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");
10295
10427
  }
10296
10428
  }
10297
10429
  // 都没有时
@@ -10316,6 +10448,7 @@
10316
10448
  apkGithubBtn.classList.remove("hidden");
10317
10449
  apkGithubBtn.onclick = function() {
10318
10450
  window.open(androidApk.github.downloadUrl, "_blank");
10451
+ safeNotify("正在打开下载页…", "info");
10319
10452
  };
10320
10453
  }
10321
10454
  }
@@ -10329,12 +10462,12 @@
10329
10462
  apkLocalBtn.textContent = "下载";
10330
10463
  apkLocalBtn.classList.remove("hidden");
10331
10464
  apkLocalBtn.onclick = function() {
10332
- window.open(androidApk.local.downloadUrl, "_self");
10465
+ triggerLocalDownload(androidApk.local.downloadUrl, androidApk.local.fileName || "wand-update.apk", apkLocalBtn);
10333
10466
  };
10334
10467
  }
10335
10468
  }
10336
10469
  if (!androidApk.github && !androidApk.local && apkMessageEl) {
10337
- apkMessageEl.textContent = "暂未提供";
10470
+ apkMessageEl.textContent = apkEnabled ? "在线版本暂时获取失败,可稍后重试" : "Android 下载未在服务端启用";
10338
10471
  apkMessageEl.classList.remove("hidden");
10339
10472
  }
10340
10473
  }
@@ -10352,7 +10485,8 @@
10352
10485
  var dmgMessageEl = document.getElementById("macos-dmg-message");
10353
10486
  var macosDmg = data.macosDmg || {};
10354
10487
  var isInMacApp = !!_macAppVersion;
10355
- var hasDmgInfo = isInMacApp || !!macosDmg.github || !!macosDmg.local;
10488
+ var dmgEnabled = macosDmg.enabled !== false;
10489
+ var hasDmgInfo = isInMacApp || (dmgEnabled && (!!macosDmg.github || !!macosDmg.local));
10356
10490
  if (dmgSection) {
10357
10491
  if (hasDmgInfo) dmgSection.classList.remove("hidden");
10358
10492
  else dmgSection.classList.add("hidden");
@@ -10370,21 +10504,8 @@
10370
10504
  dmgGithubEl.textContent = dghLabel;
10371
10505
  dmgGithubRow.classList.remove("hidden");
10372
10506
  if (dmgGithubBtn) {
10373
- dmgGithubBtn.textContent = "下载安装";
10374
- dmgGithubBtn.classList.remove("hidden");
10375
- dmgGithubBtn.onclick = function() {
10376
- try {
10377
- WandNative.downloadUpdate(macosDmg.github.downloadUrl, macosDmg.github.fileName || "wand-update.dmg", "github");
10378
- } catch (e) {
10379
- if (typeof window.wandAlert === "function") {
10380
- window.wandAlert("调用下载失败: " + e.message, { type: "danger", title: "下载失败" });
10381
- } else if (typeof showToast === "function") {
10382
- showToast("调用下载失败: " + e.message, "error");
10383
- } else {
10384
- alert("调用下载失败: " + e.message);
10385
- }
10386
- }
10387
- };
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");
10388
10509
  }
10389
10510
  }
10390
10511
  if (macosDmg.local && dmgLocalRow && dmgLocalEl) {
@@ -10393,21 +10514,8 @@
10393
10514
  dmgLocalEl.textContent = dlcLabel;
10394
10515
  dmgLocalRow.classList.remove("hidden");
10395
10516
  if (dmgLocalBtn) {
10396
- dmgLocalBtn.textContent = "下载安装";
10397
- dmgLocalBtn.classList.remove("hidden");
10398
- dmgLocalBtn.onclick = function() {
10399
- try {
10400
- WandNative.downloadUpdate(macosDmg.local.downloadUrl, macosDmg.local.fileName || "wand-update.dmg", "local");
10401
- } catch (e) {
10402
- if (typeof window.wandAlert === "function") {
10403
- window.wandAlert("调用下载失败: " + e.message, { type: "danger", title: "下载失败" });
10404
- } else if (typeof showToast === "function") {
10405
- showToast("调用下载失败: " + e.message, "error");
10406
- } else {
10407
- alert("调用下载失败: " + e.message);
10408
- }
10409
- }
10410
- };
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");
10411
10519
  }
10412
10520
  }
10413
10521
  if (!macosDmg.github && !macosDmg.local && dmgMessageEl) {
@@ -10430,6 +10538,7 @@
10430
10538
  dmgGithubBtn.classList.remove("hidden");
10431
10539
  dmgGithubBtn.onclick = function() {
10432
10540
  window.open(macosDmg.github.downloadUrl, "_blank");
10541
+ safeNotify("正在打开下载页…", "info");
10433
10542
  };
10434
10543
  }
10435
10544
  }
@@ -10442,12 +10551,12 @@
10442
10551
  dmgLocalBtn.textContent = "下载";
10443
10552
  dmgLocalBtn.classList.remove("hidden");
10444
10553
  dmgLocalBtn.onclick = function() {
10445
- window.open(macosDmg.local.downloadUrl, "_self");
10554
+ triggerLocalDownload(macosDmg.local.downloadUrl, macosDmg.local.fileName || "wand-update.dmg", dmgLocalBtn);
10446
10555
  };
10447
10556
  }
10448
10557
  }
10449
10558
  if (!macosDmg.github && !macosDmg.local && dmgMessageEl) {
10450
- dmgMessageEl.textContent = "暂未提供";
10559
+ dmgMessageEl.textContent = dmgEnabled ? "在线版本暂时获取失败,可稍后重试" : "macOS 下载未在服务端启用";
10451
10560
  dmgMessageEl.classList.remove("hidden");
10452
10561
  }
10453
10562
  }
@@ -574,9 +574,8 @@
574
574
  justify-content: center;
575
575
  padding: 0;
576
576
  box-shadow:
577
- 0 1px 3px rgba(184, 92, 55, 0.28),
578
- inset 0 1px 0 rgba(255, 255, 255, 0.28),
579
- inset 0 -1px 0 rgba(0, 0, 0, 0.08);
577
+ 0 1px 2px rgba(184, 92, 55, 0.22),
578
+ inset 0 1px 0 rgba(255, 255, 255, 0.28);
580
579
  transition:
581
580
  filter var(--transition-fast),
582
581
  box-shadow 0.22s ease,
@@ -586,40 +585,31 @@
586
585
  filter: brightness(1.06);
587
586
  transform: translateY(-1px);
588
587
  box-shadow:
589
- 0 4px 12px -2px rgba(184, 92, 55, 0.42),
590
- inset 0 1px 0 rgba(255, 255, 255, 0.35),
591
- inset 0 -1px 0 rgba(0, 0, 0, 0.08);
588
+ 0 4px 10px -3px rgba(184, 92, 55, 0.36),
589
+ inset 0 1px 0 rgba(255, 255, 255, 0.32);
592
590
  }
593
591
  /* History tile — softer cream tone to distinguish from active sessions */
594
592
  .sidebar-collapsed-tile.history {
595
593
  background: linear-gradient(145deg, #e8d3c0 0%, #d3b69d 50%, #b89478 100%);
596
594
  color: rgba(89, 58, 32, 0.88);
597
595
  box-shadow:
598
- 0 1px 3px rgba(120, 88, 56, 0.22),
599
- inset 0 1px 0 rgba(255, 255, 255, 0.45),
600
- inset 0 -1px 0 rgba(0, 0, 0, 0.06);
596
+ 0 1px 2px rgba(120, 88, 56, 0.18),
597
+ inset 0 1px 0 rgba(255, 255, 255, 0.42);
601
598
  }
602
599
  .sidebar-collapsed-tile.history:hover {
603
600
  box-shadow:
604
- 0 4px 12px -2px rgba(120, 88, 56, 0.34),
605
- inset 0 1px 0 rgba(255, 255, 255, 0.5),
606
- inset 0 -1px 0 rgba(0, 0, 0, 0.06);
601
+ 0 4px 10px -3px rgba(120, 88, 56, 0.3),
602
+ inset 0 1px 0 rgba(255, 255, 255, 0.48);
607
603
  }
608
- /* Active — liquid glass crown: outer ring + warm halo + crisp top sheen */
604
+ /* Active — accent ring + 一层柔和暖投影 + 顶高光。原先 7 层叠加在 36px 小块上
605
+ 糊成一团,精简到 3 层即可表达「升起 + 选中」。 */
609
606
  .sidebar-collapsed-tile.active {
610
607
  background: linear-gradient(145deg, #d27358 0%, #b35434 50%, #934128 100%);
611
608
  transform: translateY(-1px);
612
609
  box-shadow:
613
- /* outer glow halo */
614
- 0 0 0 1px rgba(255, 255, 255, 0.7),
615
- 0 0 0 3px rgba(197, 101, 61, 0.32),
616
- 0 8px 22px -4px rgba(160, 74, 46, 0.55),
617
- 0 3px 8px -2px rgba(160, 74, 46, 0.32),
618
- /* top sheen */
619
- inset 0 1px 0 rgba(255, 255, 255, 0.45),
620
- inset 1px 0 0 rgba(255, 255, 255, 0.18),
621
- /* bottom depth */
622
- inset 0 -1px 0 rgba(0, 0, 0, 0.16);
610
+ 0 0 0 2px rgba(197, 101, 61, 0.3),
611
+ 0 5px 14px -4px rgba(160, 74, 46, 0.45),
612
+ inset 0 1px 0 rgba(255, 255, 255, 0.4);
623
613
  }
624
614
  .sidebar-collapsed-tile:active {
625
615
  transform: translateY(0) scale(0.94);
@@ -9034,7 +9024,6 @@
9034
9024
  background: rgba(178, 79, 69, 0.12);
9035
9025
  color: var(--danger);
9036
9026
  transform: rotate(90deg);
9037
- box-shadow: 0 2px 8px rgba(178, 79, 69, 0.16);
9038
9027
  }
9039
9028
  .drawer-close-btn:active {
9040
9029
  background: rgba(178, 79, 69, 0.2);
@@ -9184,17 +9173,17 @@
9184
9173
  opacity: 1;
9185
9174
  }
9186
9175
 
9187
- /* ── Session item — clean card on glass ── */
9176
+ /* ── Session item — clean card on glass ──
9177
+ 克制原则:基础态用 hairline 描边 + 极轻顶部高光让卡片「贴」在侧栏上,
9178
+ 不给每张卡都加漂浮投影;hover/active 才升起一层柔和投影。避免多层
9179
+ box-shadow 在半透明侧栏里互相叠加显脏。 */
9188
9180
  .session-item {
9189
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.62) 0%, rgba(255, 252, 247, 0.42) 100%);
9190
- border: 0.5px solid rgba(255, 255, 255, 0.55);
9181
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.55) 0%, rgba(255, 252, 247, 0.38) 100%);
9182
+ border: 1px solid rgba(125, 91, 57, 0.08);
9191
9183
  border-radius: 13px;
9192
9184
  padding: 11px 14px;
9193
9185
  margin-bottom: 6px;
9194
- box-shadow:
9195
- 0 0 0 0.5px rgba(125, 91, 57, 0.05),
9196
- 0 1px 2px rgba(125, 91, 57, 0.03),
9197
- inset 0 1px 0 rgba(255, 255, 255, 0.6);
9186
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
9198
9187
  transition:
9199
9188
  background 0.2s ease,
9200
9189
  border-color 0.2s ease,
@@ -9211,53 +9200,39 @@
9211
9200
  }
9212
9201
  .session-item:hover {
9213
9202
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.85) 0%, rgba(255, 253, 248, 0.62) 100%);
9214
- border-color: rgba(255, 255, 255, 0.7);
9203
+ border-color: rgba(125, 91, 57, 0.14);
9215
9204
  transform: translateY(-1px);
9216
- box-shadow:
9217
- 0 0 0 0.5px rgba(125, 91, 57, 0.08),
9218
- 0 6px 16px -6px rgba(125, 91, 57, 0.18),
9219
- inset 0 1px 0 rgba(255, 255, 255, 0.8);
9205
+ box-shadow: 0 4px 12px -4px rgba(125, 91, 57, 0.16);
9220
9206
  }
9221
9207
  .session-item:hover::before {
9222
9208
  opacity: 0.5;
9223
9209
  transform: scaleY(1);
9224
9210
  }
9225
- /* Selected (active) — true liquid glass pill with depth + accent glow */
9211
+ /* Selected (active) — accent ring 用边框表达,再叠一层柔和暖投影 + 顶高光。
9212
+ 去掉原先的 6 层叠加(外环/光晕/底影/三向 inset),层数过多在玻璃上显脏。 */
9226
9213
  .session-item.active {
9227
9214
  background:
9228
9215
  linear-gradient(180deg, rgba(255, 252, 247, 0.92) 0%, rgba(255, 244, 232, 0.78) 100%);
9229
- border: 0.5px solid rgba(255, 255, 255, 0.85);
9216
+ border: 1px solid rgba(197, 101, 61, 0.32);
9230
9217
  transform: translateY(-1px);
9231
9218
  backdrop-filter: blur(20px) saturate(180%);
9232
9219
  -webkit-backdrop-filter: blur(20px) saturate(180%);
9233
9220
  box-shadow:
9234
- /* outer accent ring (soft) */
9235
- 0 0 0 1px rgba(197, 101, 61, 0.28),
9236
- /* warm glow halo */
9237
- 0 8px 28px -6px rgba(197, 101, 61, 0.32),
9238
- /* base elevation */
9239
- 0 2px 6px -1px rgba(125, 91, 57, 0.1),
9240
- /* top inner highlight — light from above */
9241
- inset 0 1px 0 rgba(255, 255, 255, 0.95),
9242
- /* bottom inner shadow — depth at base */
9243
- inset 0 -1px 0 rgba(197, 101, 61, 0.08),
9244
- /* left inner glint */
9245
- inset 1px 0 0 rgba(255, 255, 255, 0.4);
9221
+ 0 4px 14px -4px rgba(197, 101, 61, 0.24),
9222
+ inset 0 1px 0 rgba(255, 255, 255, 0.7);
9246
9223
  }
9224
+ /* ::before 的左侧高亮条受父级 overflow:hidden 裁切,原来的 box-shadow 光晕
9225
+ 根本显示不出来,移除以省一次绘制。 */
9247
9226
  .session-item.active::before {
9248
9227
  opacity: 1;
9249
9228
  transform: scaleY(1);
9250
- box-shadow: 0 0 8px rgba(197, 101, 61, 0.5);
9251
9229
  }
9252
9230
  /* Multi-select state — quieter glass tint, no accent halo */
9253
9231
  .session-item.selected {
9254
9232
  background:
9255
9233
  linear-gradient(180deg, rgba(255, 250, 244, 0.85) 0%, rgba(255, 246, 236, 0.65) 100%);
9256
- border: 0.5px solid rgba(255, 255, 255, 0.7);
9257
- box-shadow:
9258
- 0 0 0 1px rgba(197, 101, 61, 0.18),
9259
- 0 4px 14px -4px rgba(125, 91, 57, 0.14),
9260
- inset 0 1px 0 rgba(255, 255, 255, 0.85);
9234
+ border: 1px solid rgba(197, 101, 61, 0.2);
9235
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
9261
9236
  }
9262
9237
  .session-item.selected::before {
9263
9238
  opacity: 0.7;
@@ -9402,21 +9377,19 @@
9402
9377
  transform: scale(0.96);
9403
9378
  transition-duration: 0.06s;
9404
9379
  }
9405
- /* Footer tab active — same liquid glass language as .session-item.active */
9380
+ /* Footer tab active — same liquid glass language as .session-item.active
9381
+ (同步精简为 accent 边框 + 1 层柔和投影 + 顶高光) */
9406
9382
  .sidebar-footer-actions .btn.active {
9407
9383
  background:
9408
9384
  linear-gradient(180deg, rgba(255, 252, 247, 0.95) 0%, rgba(255, 242, 228, 0.78) 100%);
9409
- border: 0.5px solid rgba(255, 255, 255, 0.85);
9385
+ border: 1px solid rgba(197, 101, 61, 0.28);
9410
9386
  color: var(--accent);
9411
9387
  transform: translateY(-1px);
9412
9388
  backdrop-filter: blur(16px) saturate(180%);
9413
9389
  -webkit-backdrop-filter: blur(16px) saturate(180%);
9414
9390
  box-shadow:
9415
- 0 0 0 1px rgba(197, 101, 61, 0.28),
9416
- 0 6px 18px -4px rgba(197, 101, 61, 0.32),
9417
- 0 2px 4px -1px rgba(125, 91, 57, 0.08),
9418
- inset 0 1px 0 rgba(255, 255, 255, 0.95),
9419
- inset 0 -1px 0 rgba(197, 101, 61, 0.08);
9391
+ 0 3px 10px -3px rgba(197, 101, 61, 0.22),
9392
+ inset 0 1px 0 rgba(255, 255, 255, 0.7);
9420
9393
  }
9421
9394
  .sidebar-footer-actions .btn.sidebar-logout:hover {
9422
9395
  background: rgba(178, 79, 69, 0.08);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "1.33.0",
3
+ "version": "1.35.0",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {