@co0ontty/wand 1.55.1 → 1.55.2-beta.g7d9f661

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/README.md CHANGED
@@ -76,7 +76,6 @@ bash <(curl -Ls https://raw.githubusercontent.com/co0ontty/wand/master/install.s
76
76
 
77
77
  ### 部署与访问
78
78
 
79
- - **PWA 支持** — 可添加到主屏幕作为独立应用使用
80
79
  - **Android 客户端** — WebView 壳应用,支持加密连接码分发、APK 自动更新检查、原生通知推送、启动器图标切换
81
80
  - **HTTPS** — 可选自签证书,适合远程或移动端访问
82
81
  - **版本管理** — 内置更新检查与升级提示
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "21bfed5335bc4c28e0ad119ca4e0d516a8bb3887",
3
- "builtAt": "2026-06-12T03:44:54.824Z",
4
- "version": "1.55.1",
5
- "channel": "stable"
2
+ "commit": "7d9f661e6162bf1addbb69a57ac323b724157072",
3
+ "builtAt": "2026-06-12T09:57:42.943Z",
4
+ "version": "1.55.2-beta.g7d9f661",
5
+ "channel": "beta"
6
6
  }
package/dist/cert.js CHANGED
@@ -164,7 +164,7 @@ export function ensureCertificates(configDir, options = {}) {
164
164
  const fingerprint = computeFingerprint(ssl.cert);
165
165
  process.stdout.write(`[wand] 证书已写入 ${paths.certPath}\n` +
166
166
  `[wand] SHA-256 指纹: ${fingerprint}\n` +
167
- `[wand] 注意:自签证书浏览器会标红;PWA / Service Worker 需要把它导入受信任根证书才能工作。\n` +
167
+ `[wand] 注意:自签证书浏览器会标红;可将它导入受信任根证书以消除警告。\n` +
168
168
  `[wand] HTTPS 启用后可通过 GET /cert/server.crt 在客户端下载安装。\n`);
169
169
  return { ...ssl, certPath: paths.certPath, fingerprint, userProvided: false };
170
170
  }
package/dist/config.d.ts CHANGED
@@ -14,7 +14,6 @@ export declare const defaultConfig: () => WandConfig;
14
14
  export declare function resolveConfigPath(inputPath?: string): string;
15
15
  export declare function resolveConfigDir(configPath: string): string;
16
16
  export declare function hasConfigFile(configPath: string): boolean;
17
- export declare function ensureConfig(configPath: string): Promise<WandConfig>;
18
17
  /** saveConfig 写出时去掉偏好字段——这些已经移到 SQLite。 */
19
18
  export declare function saveConfig(configPath: string, config: WandConfig): Promise<void>;
20
19
  /**
package/dist/config.js CHANGED
@@ -107,25 +107,6 @@ async function atomicWriteFile(filePath, content) {
107
107
  throw err;
108
108
  }
109
109
  }
110
- export async function ensureConfig(configPath) {
111
- const dir = path.dirname(configPath);
112
- await mkdir(dir, { recursive: true });
113
- try {
114
- const raw = await readFile(configPath, "utf8");
115
- const merged = mergeWithDefaults(JSON.parse(raw));
116
- const normalized = `${JSON.stringify(merged, null, 2)}\n`;
117
- // Only write if the file content actually changed
118
- if (raw.trimEnd() !== normalized.trimEnd()) {
119
- await atomicWriteFile(configPath, normalized);
120
- }
121
- return merged;
122
- }
123
- catch {
124
- const config = defaultConfig();
125
- await atomicWriteFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
126
- return config;
127
- }
128
- }
129
110
  /** saveConfig 写出时去掉偏好字段——这些已经移到 SQLite。 */
130
111
  export async function saveConfig(configPath, config) {
131
112
  await mkdir(path.dirname(configPath), { recursive: true });
@@ -25,8 +25,6 @@ export declare function normalizeUpdateChannel(value: unknown): UpdateChannel;
25
25
  export declare function getUpdateDistTag(channel: UpdateChannel): "latest" | "beta";
26
26
  export declare function getInstallSpecForChannel(channel: UpdateChannel): string;
27
27
  export declare function getStableTagVersion(version: string): string;
28
- export declare function isBetaPackageVersion(version: string): boolean;
29
- export declare function isPureStableVersion(version: string): boolean;
30
28
  export declare function buildPackageUpdateInfo(currentVersion: string, channel: UpdateChannel, latestVersion: string | null): PackageUpdateInfo;
31
29
  export declare function checkPackageUpdateAsync(currentVersion: string, channel: UpdateChannel, timeoutMs?: number): Promise<PackageUpdateInfo>;
32
30
  export declare function checkPackageUpdateSync(currentVersion: string, channel: UpdateChannel, timeoutMs?: number): PackageUpdateInfo;
@@ -43,12 +43,6 @@ function cleanVersion(value) {
43
43
  export function getStableTagVersion(version) {
44
44
  return cleanVersion(version).split("+")[0]?.split("-")[0] ?? cleanVersion(version);
45
45
  }
46
- export function isBetaPackageVersion(version) {
47
- return /(?:^|-)beta(?:[.-]|$)/i.test(cleanVersion(version));
48
- }
49
- export function isPureStableVersion(version) {
50
- return /^\d+\.\d+\.\d+$/.test(cleanVersion(version));
51
- }
52
46
  function computeUpdateAvailable(currentVersion, latestVersion, channel) {
53
47
  if (!latestVersion)
54
48
  return false;
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import crypto from "node:crypto";
2
- import { compareSemver, extractSemver } from "./version-utils.js";
2
+ import { compareApkInstallOrder, compareSemver, extractSemver } from "./version-utils.js";
3
3
  import compression from "compression";
4
4
  import express from "express";
5
5
  import { createReadStream, existsSync, readFileSync, writeFileSync } from "node:fs";
@@ -11,7 +11,6 @@ import { promisify } from "node:util";
11
11
  import path from "node:path";
12
12
  import process from "node:process";
13
13
  import { WebSocketServer } from "ws";
14
- import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
15
14
  import { createSession, readSessionCookie, revokeSession, SESSION_COOKIE_HTTP, SESSION_COOKIE_HTTPS, SESSION_COOKIE_LEGACY, setAuthStorage, validateSession, } from "./auth.js";
16
15
  import { ensureCertificates } from "./cert.js";
17
16
  import { buildChildEnv } from "./env-utils.js";
@@ -20,7 +19,6 @@ import { getCachedModels, refreshModels } from "./models.js";
20
19
  import { ProcessManager } from "./process-manager.js";
21
20
  import { SessionLogger } from "./session-logger.js";
22
21
  import { StructuredSessionManager } from "./structured-session-manager.js";
23
- import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
24
22
  import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
25
23
  import { checkPackageUpdateAsync, installPackageGloballyAsync, normalizeUpdateChannel, resolveGlobalWandCli, } from "./npm-update-utils.js";
26
24
  import { repairServiceUnitAfterUpdate } from "./service-self-repair.js";
@@ -140,30 +138,39 @@ async function fetchGitHubLatestApk(forceRefresh = false) {
140
138
  }
141
139
  }
142
140
  async function resolveLatestApkVersion(configDir, config) {
143
- // Priority 1: local APK file
141
+ // local github 两个来源都看,按安装序取真正更新的那个(持平偏向 local:同源下载更快)。
142
+ // 旧逻辑是「local 存在就一票否决」——本地目录留着旧包时,会把线上新版压住不提示。
144
143
  const localApk = await resolveAndroidApkAsset(configDir, config);
145
- if (localApk && localApk.version) {
146
- return {
144
+ const local = localApk && localApk.version
145
+ ? {
147
146
  version: localApk.version,
148
147
  downloadUrl: localApk.downloadUrl,
149
148
  fileName: localApk.fileName,
150
149
  size: localApk.size,
151
150
  source: "local",
152
- };
151
+ }
152
+ : null;
153
+ let github = null;
154
+ try {
155
+ const ghApk = await fetchGitHubLatestApk();
156
+ if (ghApk) {
157
+ github = {
158
+ version: ghApk.version,
159
+ downloadUrl: ghApk.downloadUrl,
160
+ fileName: ghApk.fileName,
161
+ size: ghApk.size,
162
+ source: "github",
163
+ releaseNotes: ghApk.releaseNotes,
164
+ };
165
+ }
153
166
  }
154
- // Priority 2: GitHub Release
155
- const ghApk = await fetchGitHubLatestApk();
156
- if (ghApk) {
157
- return {
158
- version: ghApk.version,
159
- downloadUrl: ghApk.downloadUrl,
160
- fileName: ghApk.fileName,
161
- size: ghApk.size,
162
- source: "github",
163
- releaseNotes: ghApk.releaseNotes,
164
- };
167
+ catch {
168
+ // GitHub 不可达时静默回退 local
165
169
  }
166
- return null;
170
+ if (local && github) {
171
+ return compareApkInstallOrder(github.version, local.version) > 0 ? github : local;
172
+ }
173
+ return local ?? github;
167
174
  }
168
175
  let cachedGitHubDmg = null;
169
176
  let gitHubDmgCacheTs = 0;
@@ -526,13 +533,15 @@ async function resolveAndroidApkAsset(configDir, config) {
526
533
  fileStat,
527
534
  };
528
535
  }));
529
- // 按语义版本选"最新", 而非修改时间 —— cp/rsync/解压/checkout 都可能让低版本号文件的
536
+ // 按版本号选"最新", 而非修改时间 —— cp/rsync/解压/checkout 都可能让低版本号文件的
530
537
  // mtime 更新, 用 mtime 会把旧版本号当成 latest 上报。版本相同或都无版本号时退回 mtime。
538
+ // 注意用安装序比较(同三段时 debug > release,镜像 versionCode),不是标准 semver:
539
+ // wand-v1.55.0.apk 与 wand-v1.55.0-debug.x.apk 并存时,debug 才是装得上的更新包。
531
540
  candidates.sort((a, b) => {
532
541
  const va = extractAndroidApkVersion(a.entry.name);
533
542
  const vb = extractAndroidApkVersion(b.entry.name);
534
543
  if (va && vb) {
535
- const cmp = compareSemver(vb, va);
544
+ const cmp = compareApkInstallOrder(vb, va);
536
545
  if (cmp !== 0)
537
546
  return cmp;
538
547
  }
@@ -925,7 +934,6 @@ export async function startServer(config, configPath) {
925
934
  const storage = new WandStorage(resolveDatabasePath(configPath));
926
935
  setAuthStorage(storage);
927
936
  const configDir = resolveConfigDir(configPath);
928
- const avatarSeed = await ensureAvatarSeed(configDir);
929
937
  const processes = new ProcessManager(config, storage, configDir);
930
938
  const structuredLogger = new SessionLogger(configDir, config.shortcutLogMaxBytes);
931
939
  const structuredSessions = new StructuredSessionManager(storage, config, structuredLogger);
@@ -943,7 +951,7 @@ export async function startServer(config, configPath) {
943
951
  app.get("/vendor/wterm/wterm.bundle.js", (req, res) => sendEmbeddedVendorAsset("/vendor/wterm/wterm.bundle.js", req, res));
944
952
  app.get("/vendor/wterm/terminal.css", (req, res) => sendEmbeddedVendorAsset("/vendor/wterm/terminal.css", req, res));
945
953
  app.get("/vendor/qrcode/qrcode.bundle.js", (req, res) => sendEmbeddedVendorAsset("/vendor/qrcode/qrcode.bundle.js", req, res));
946
- // ── Web UI and PWA endpoints ──
954
+ // ── Web UI endpoints ──
947
955
  app.get("/", (_req, res) => {
948
956
  res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
949
957
  res.type("html").send(renderApp(configPath));
@@ -992,24 +1000,6 @@ export async function startServer(config, configPath) {
992
1000
  res.status(404).end();
993
1001
  }
994
1002
  });
995
- app.get("/manifest.json", (_req, res) => {
996
- res.setHeader("Content-Type", "application/manifest+json");
997
- res.send(generatePwaManifest());
998
- });
999
- for (const [route, size] of [["/icon.svg", 192], ["/icon-192.png", 192], ["/icon-512.png", 512]]) {
1000
- app.get(route, (_req, res) => {
1001
- res.type("image/svg+xml").send(getAvatarSvg(avatarSeed, size));
1002
- });
1003
- }
1004
- app.get("/sw.js", (_req, res) => {
1005
- res.setHeader("Content-Type", "application/javascript");
1006
- res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
1007
- res.setHeader("Service-Worker-Allowed", "/");
1008
- res.send(generateServiceWorker());
1009
- });
1010
- app.get("/offline", (_req, res) => {
1011
- res.type("html").send(renderApp(configPath));
1012
- });
1013
1003
  // ── Auth routes ──
1014
1004
  app.post("/api/login", (req, res) => {
1015
1005
  const clientIp = req.ip || req.socket.remoteAddress || "unknown";
@@ -1091,7 +1081,9 @@ export async function startServer(config, configPath) {
1091
1081
  res.json({ updateAvailable: false, currentVersion, latestVersion: null, downloadUrl: null, source: null });
1092
1082
  return;
1093
1083
  }
1094
- const updateAvailable = compareSemver(latest.version, currentVersion) > 0;
1084
+ // 安装序比较(镜像 versionCode),不是标准 semver:只在系统安装器真能装上时才提示,
1085
+ // 避免「提示升级 → 下载 → 被按降级拒装」的死循环(如已装 1.55.0-debug 提示装 1.55.0)。
1086
+ const updateAvailable = compareApkInstallOrder(latest.version, currentVersion) > 0;
1095
1087
  res.json({
1096
1088
  updateAvailable,
1097
1089
  currentVersion,
package/dist/types.d.ts CHANGED
@@ -33,9 +33,6 @@ export interface TurnRequest {
33
33
  approvalPolicy?: ApprovalPolicy;
34
34
  allowedScopes?: EscalationScope[];
35
35
  }
36
- export interface EscalationDecisionRequest {
37
- resolution?: Extract<EscalationResolution, "approve_once" | "approve_turn" | "deny">;
38
- }
39
36
  export interface CommandPreset {
40
37
  label: string;
41
38
  command: string;
@@ -79,7 +76,7 @@ export interface WandConfig {
79
76
  /**
80
77
  * 可选:使用用户自备的 TLS 证书/私钥(PEM 格式)。配了 `certPath` + `keyPath`
81
78
  * 且文件可读时,将跳过自签证书生成。用 mkcert/Let's Encrypt 等签的证书时,
82
- * 浏览器视为受信任,PWA / Service Worker 才能正常注册。
79
+ * 浏览器可直接信任 HTTPS 连接。
83
80
  */
84
81
  tls?: {
85
82
  certPath?: string;
@@ -14,3 +14,19 @@ export declare function extractSemver(text: string): string | null;
14
14
  * 贴近标准 semver,避免 debug.MMDDHHMM 后缀因纯字典序而跨月/跨年排反。
15
15
  */
16
16
  export declare function compareSemver(a: string, b: string): number;
17
+ /**
18
+ * Android APK 安装序比较 —— 与 android/app/build.gradle 的 computeVersionCode 镜像一致,
19
+ * 回答「这个包能否覆盖安装到那个包之上 / 该不该提示升级」。返回正数 = a 比 b 新。
20
+ *
21
+ * 与标准 semver(compareSemver)的关键差异:同主版本三段时,带 `-debug` 后缀的包
22
+ * 【更新】而不是更旧 —— debug 包是 tag 之后的 master 构建(versionCode = base+1,
23
+ * release 是 base+0),系统安装器只认 versionCode。若在这里沿用 semver 的
24
+ * 「prerelease < release」规则,就会提示用户从 debug「升级」到同号 release,
25
+ * 下载后被系统按降级拒装。
26
+ *
27
+ * - 三段数值比较优先;
28
+ * - 同三段:带 -debug > 不带(镜像 versionCode base+1 > base+0);
29
+ * - 两个 debug:按后缀分段比较(debug.MMDDHHMM 时间戳数值比),versionCode 相同、
30
+ * 系统允许互装,比较结果只用于「是否提示更新」。
31
+ */
32
+ export declare function compareApkInstallOrder(a: string, b: string): number;
@@ -37,8 +37,12 @@ export function compareSemver(a, b) {
37
37
  return -1;
38
38
  if (!pa.pre && !pb.pre)
39
39
  return 0;
40
- const segA = pa.pre.split(".");
41
- const segB = pb.pre.split(".");
40
+ return comparePrereleaseSegments(pa.pre, pb.pre);
41
+ }
42
+ /** 按 `.` 分段比较 prerelease 后缀:数字段数值比、非数字段字典序、段少者更小。 */
43
+ function comparePrereleaseSegments(preA, preB) {
44
+ const segA = preA.split(".");
45
+ const segB = preB.split(".");
42
46
  const segLen = Math.max(segA.length, segB.length);
43
47
  for (let i = 0; i < segLen; i++) {
44
48
  const sa = segA[i];
@@ -64,3 +68,38 @@ export function compareSemver(a, b) {
64
68
  }
65
69
  return 0;
66
70
  }
71
+ /**
72
+ * Android APK 安装序比较 —— 与 android/app/build.gradle 的 computeVersionCode 镜像一致,
73
+ * 回答「这个包能否覆盖安装到那个包之上 / 该不该提示升级」。返回正数 = a 比 b 新。
74
+ *
75
+ * 与标准 semver(compareSemver)的关键差异:同主版本三段时,带 `-debug` 后缀的包
76
+ * 【更新】而不是更旧 —— debug 包是 tag 之后的 master 构建(versionCode = base+1,
77
+ * release 是 base+0),系统安装器只认 versionCode。若在这里沿用 semver 的
78
+ * 「prerelease < release」规则,就会提示用户从 debug「升级」到同号 release,
79
+ * 下载后被系统按降级拒装。
80
+ *
81
+ * - 三段数值比较优先;
82
+ * - 同三段:带 -debug > 不带(镜像 versionCode base+1 > base+0);
83
+ * - 两个 debug:按后缀分段比较(debug.MMDDHHMM 时间戳数值比),versionCode 相同、
84
+ * 系统允许互装,比较结果只用于「是否提示更新」。
85
+ */
86
+ export function compareApkInstallOrder(a, b) {
87
+ const parse = (v) => {
88
+ const [main, ...rest] = v.replace(/^v/, "").split("-");
89
+ const pre = rest.join("-");
90
+ const mainParts = main.split(".").map((n) => Number(n) || 0);
91
+ return { mainParts, pre, isDebug: pre.startsWith("debug") };
92
+ };
93
+ const pa = parse(a);
94
+ const pb = parse(b);
95
+ for (let i = 0; i < 3; i++) {
96
+ const diff = (pa.mainParts[i] || 0) - (pb.mainParts[i] || 0);
97
+ if (diff !== 0)
98
+ return diff;
99
+ }
100
+ if (pa.isDebug !== pb.isDebug)
101
+ return pa.isDebug ? 1 : -1;
102
+ if (pa.isDebug && pb.isDebug)
103
+ return comparePrereleaseSegments(pa.pre, pb.pre);
104
+ return 0;
105
+ }