@co0ontty/wand 4.52.0 → 4.53.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.
@@ -3,8 +3,9 @@ import { pipeline } from "node:stream/promises";
3
3
  import { getErrorMessage } from "./error-utils.js";
4
4
  import { asyncRoute } from "./express-async.js";
5
5
  import { checkProviderCliUpdates, updateProviderClis, verifyProviderCliUpdateResults, } from "./provider-cli-updater.js";
6
+ import { absoluteUrl, buildIosInstallPage, buildIosOtaManifest, buildItmsServicesUrl, collectIosOtaBlockers, formatIosAssetSize, inspectIpa, normalizePublicOrigin, publicOriginFromRequest, } from "./ios-ota.js";
6
7
  import { streamFileWithRange } from "./server-file-routes.js";
7
- import { compareApkInstallOrder, compareSemver } from "./version-utils.js";
8
+ import { compareApkInstallOrder, compareSemver, compareWandInstallOrder } from "./version-utils.js";
8
9
  import { canUseDetachedUpdateHelper, startDetachedUpdateHelper } from "./update-helper.js";
9
10
  const GITHUB_PROXY_TIMEOUT_MS = 5 * 60 * 1000;
10
11
  async function proxyGitHubApk(_req, res, asset, fetchImpl = fetch) {
@@ -160,6 +161,78 @@ export function registerPublicUpdateRoutes(app, deps) {
160
161
  readErrorMessage: "读取 DMG 文件失败。",
161
162
  });
162
163
  }));
164
+ app.get("/api/ios-ipa-update", asyncRoute(async (req, res) => {
165
+ const currentVersion = typeof req.query.currentVersion === "string" ? req.query.currentVersion.trim() : "";
166
+ if (!currentVersion) {
167
+ res.status(400).json({ error: "Missing currentVersion query parameter." });
168
+ return;
169
+ }
170
+ const payload = await resolveIosOtaPayload(req, deps);
171
+ if (!payload) {
172
+ res.json({
173
+ updateAvailable: false,
174
+ currentVersion,
175
+ latestVersion: null,
176
+ downloadUrl: null,
177
+ installUrl: null,
178
+ manifestUrl: null,
179
+ installPageUrl: null,
180
+ fileName: null,
181
+ size: null,
182
+ source: null,
183
+ signed: false,
184
+ otaReady: false,
185
+ otaBlockers: [],
186
+ bundleId: null,
187
+ });
188
+ return;
189
+ }
190
+ const updateAvailable = compareWandInstallOrder(payload.latestVersion, currentVersion) > 0;
191
+ res.json({
192
+ updateAvailable,
193
+ currentVersion,
194
+ latestVersion: payload.latestVersion,
195
+ downloadUrl: updateAvailable ? payload.downloadUrl : null,
196
+ installUrl: updateAvailable ? payload.installUrl : null,
197
+ manifestUrl: payload.manifestUrl,
198
+ installPageUrl: payload.installPageUrl,
199
+ fileName: updateAvailable ? payload.fileName : null,
200
+ size: updateAvailable ? payload.size : null,
201
+ source: payload.source,
202
+ signed: payload.signed,
203
+ otaReady: payload.otaReady,
204
+ otaBlockers: payload.otaBlockers,
205
+ bundleId: payload.bundleId,
206
+ });
207
+ }));
208
+ app.get("/ios/manifest.plist", asyncRoute(async (req, res) => {
209
+ const payload = await resolveIosOtaPayload(req, deps);
210
+ if (!payload) {
211
+ res.status(404).type("text/plain").send("当前没有可安装的 IPA 文件。");
212
+ return;
213
+ }
214
+ res.setHeader("Content-Type", "text/xml; charset=utf-8");
215
+ res.setHeader("Cache-Control", "no-cache");
216
+ res.send(payload.manifest);
217
+ }));
218
+ app.get("/ios/install", asyncRoute(async (req, res) => {
219
+ const payload = await resolveIosOtaPayload(req, deps);
220
+ if (!payload) {
221
+ res.status(404).type("text/plain").send("当前没有可安装的 IPA 文件。");
222
+ return;
223
+ }
224
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
225
+ res.setHeader("Cache-Control", "no-cache");
226
+ res.send(buildIosInstallPage({
227
+ title: payload.title,
228
+ version: payload.latestVersion,
229
+ sizeLabel: formatIosAssetSize(payload.size),
230
+ installUrl: payload.installUrl ?? "#",
231
+ downloadUrl: payload.downloadUrl,
232
+ signed: payload.signed,
233
+ https: payload.origin.startsWith("https://"),
234
+ }));
235
+ }));
163
236
  app.get("/ios/download", asyncRoute(async (req, res) => {
164
237
  const asset = await deps.resolveIosDownload();
165
238
  if (!asset) {
@@ -175,6 +248,67 @@ export function registerPublicUpdateRoutes(app, deps) {
175
248
  });
176
249
  }));
177
250
  }
251
+ async function resolveIosOtaPayload(req, deps) {
252
+ const latest = await deps.resolveLatestIpa();
253
+ const local = await deps.resolveIosDownload();
254
+ if (!latest && !local)
255
+ return null;
256
+ const origin = normalizePublicOrigin(typeof req.query.origin === "string" ? req.query.origin : undefined)
257
+ ?? publicOriginFromRequest(req)
258
+ ?? "";
259
+ const source = local ? "local" : (latest?.source ?? "github");
260
+ const fileName = local?.fileName ?? latest?.fileName ?? "wand.ipa";
261
+ const size = local?.size ?? latest?.size ?? 0;
262
+ const latestVersion = latest?.version ?? "0.0.0";
263
+ const metadata = local
264
+ ? await inspectIpa(local.filePath, { version: latestVersion }).catch(() => ({
265
+ bundleId: "com.wand.app",
266
+ bundleVersion: latestVersion,
267
+ bundleBuild: latestVersion,
268
+ title: "Wand",
269
+ signed: false,
270
+ }))
271
+ : {
272
+ bundleId: "com.wand.app",
273
+ bundleVersion: latestVersion,
274
+ bundleBuild: latestVersion,
275
+ title: "Wand",
276
+ signed: false,
277
+ };
278
+ const downloadPath = latest?.downloadUrl ?? "/ios/download";
279
+ const downloadUrl = origin ? absoluteUrl(origin, downloadPath) : downloadPath;
280
+ const manifestUrl = origin ? absoluteUrl(origin, "/ios/manifest.plist") : "/ios/manifest.plist";
281
+ const installPageUrl = origin ? absoluteUrl(origin, "/ios/install") : "/ios/install";
282
+ const blockers = collectIosOtaBlockers({
283
+ signed: metadata.signed,
284
+ origin,
285
+ source,
286
+ });
287
+ const otaReady = blockers.length === 0;
288
+ const manifest = buildIosOtaManifest({
289
+ ipaUrl: downloadUrl,
290
+ bundleId: metadata.bundleId,
291
+ bundleVersion: metadata.bundleVersion,
292
+ title: metadata.title,
293
+ });
294
+ return {
295
+ latestVersion: metadata.bundleVersion || latestVersion,
296
+ downloadUrl,
297
+ installUrl: origin ? buildItmsServicesUrl(manifestUrl) : null,
298
+ manifestUrl,
299
+ installPageUrl,
300
+ fileName,
301
+ size,
302
+ source,
303
+ signed: metadata.signed,
304
+ otaReady,
305
+ otaBlockers: blockers,
306
+ bundleId: metadata.bundleId,
307
+ title: metadata.title,
308
+ origin,
309
+ manifest,
310
+ };
311
+ }
178
312
  export class ServerUpdateState {
179
313
  providerCliUpdateCache = null;
180
314
  providerCliUpdateInFlight = false;
@@ -16,6 +16,29 @@ function workspaceSessionTitle(session, names = {}) {
16
16
  cwd: session.cwd,
17
17
  }));
18
18
  }
19
+ function liveSession(session, registry) {
20
+ return registry?.getLatest(session.id) ?? session;
21
+ }
22
+ function workspaceSessionSummary(session, names = {}, registry) {
23
+ const live = liveSession(session, registry);
24
+ return {
25
+ id: live.id,
26
+ provider: live.provider,
27
+ sessionKind: live.sessionKind,
28
+ runner: live.runner,
29
+ command: live.command,
30
+ title: workspaceSessionTitle(live, names),
31
+ status: live.status,
32
+ ptyBusy: live.ptyBusy === true,
33
+ providerCliActive: live.providerCliActive,
34
+ inFlight: live.structuredState?.inFlight === true,
35
+ cwd: live.cwd,
36
+ startedAt: live.startedAt,
37
+ };
38
+ }
39
+ function tasksRevision(groups) {
40
+ return crypto.createHash("sha256").update(JSON.stringify(groups)).digest("base64url");
41
+ }
19
42
  function parseDefaultProvider(value) {
20
43
  return typeof value === "string" && PROVIDERS.has(value) ? value : undefined;
21
44
  }
@@ -196,8 +219,8 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
196
219
  res.json({
197
220
  ...workspaceWithCounts(storage, workspace),
198
221
  sessions: storage.listSessionsByWorkspace(workspace.id).map((session) => ({
199
- ...session,
200
- title: workspaceSessionTitle(session, { workspaceName: workspace.name }),
222
+ ...workspaceSessionSummary(session, { workspaceName: workspace.name }, sessions),
223
+ workspaceTaskId: session.workspaceTaskId,
201
224
  })),
202
225
  });
203
226
  });
@@ -288,17 +311,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
288
311
  const sessionLimit = parseBoundedCount(req.query.maxSessions);
289
312
  backfillSessionWorkspaces(storage);
290
313
  const workspaces = storage.listWorkspaces();
291
- const summarize = (session, names = {}) => ({
292
- id: session.id,
293
- provider: session.provider,
294
- sessionKind: session.sessionKind,
295
- runner: session.runner,
296
- command: session.command,
297
- title: workspaceSessionTitle(session, names),
298
- status: session.status,
299
- cwd: session.cwd,
300
- startedAt: session.startedAt,
301
- });
314
+ const summarize = (session, names = {}) => workspaceSessionSummary(session, names, sessions);
302
315
  const visibleWorkspaces = workspaceFilter
303
316
  ? workspaces.filter((workspace) => workspace.id === workspaceFilter)
304
317
  : workspaces;
@@ -369,7 +382,17 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
369
382
  workspaceName: group.workspaceName,
370
383
  }));
371
384
  }
372
- res.json([...groups.values()]);
385
+ const payload = [...groups.values()];
386
+ const revision = tasksRevision(payload);
387
+ if (typeof req.query.revision === "string") {
388
+ if (req.query.revision === revision) {
389
+ res.json({ unchanged: true, revision, groups: [] });
390
+ return;
391
+ }
392
+ res.json({ unchanged: false, revision, groups: payload });
393
+ return;
394
+ }
395
+ res.json(payload);
373
396
  });
374
397
  // 列出某工作空间下的任务
375
398
  app.get("/api/workspaces/:id/tasks", (req, res) => {
@@ -510,11 +533,11 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
510
533
  ...task,
511
534
  cwd: task.worktree?.path ?? workspace?.cwd ?? "",
512
535
  sessions: storage.listSessionsByWorkspaceTask(task.id).map((session) => ({
513
- ...session,
514
- title: workspaceSessionTitle(session, {
536
+ ...workspaceSessionSummary(session, {
515
537
  taskName: task.name,
516
538
  workspaceName: workspace?.name,
517
- }),
539
+ }, sessions),
540
+ workspaceTaskId: task.id,
518
541
  })),
519
542
  });
520
543
  });
@@ -1,5 +1,5 @@
1
1
  import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
2
- import { shouldAcceptGeneratedSessionTitle, summarizeSessionTitleFromInput, } from "./session-topic.js";
2
+ import { collectSessionTopicBlocklist, shouldAcceptGeneratedSessionTitle, summarizeSessionTitleFromInput, } from "./session-topic.js";
3
3
  export const SESSION_TRANSPORT_OUTPUT_LIMIT = 200_000;
4
4
  function cleanDisplayTitle(value) {
5
5
  return value?.replace(/\s+/g, " ").trim().slice(0, 40) ?? "";
@@ -61,7 +61,7 @@ function sessionBase(snapshot) {
61
61
  autoApprovePermissions: snapshot.autoApprovePermissions,
62
62
  approvalStats: snapshot.approvalStats,
63
63
  summary: snapshot.summary,
64
- title: resolveSessionDisplayTitle(snapshot),
64
+ title: resolveSessionDisplayTitle(snapshot, collectSessionTopicBlocklist({ cwd: snapshot.cwd })),
65
65
  description: snapshot.description,
66
66
  titleGenerating: snapshot.titleGenerating,
67
67
  currentTaskTitle: snapshot.currentTaskTitle,