@marktrue/dsh-mobile-remote 3.2.1-alpha.3 → 3.2.1-rc.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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.2.1-rc.1(build 26,issue #39)— NPM-only 自动更新(v3.2.1 线)
4
+
5
+ > 插件 `3.2.1-rc.1` / App `3.2.1+26` / 载体 `3.2.1-rc.1-app`。本 fork 的 v3.2.1 预发布候选;仅发布到本地 registry 做本机验收,不触发公共 npm 发布或 tag。
6
+
7
+ ### 更新源与安全
8
+
9
+ - 插件更新唯一来源为宿主有效 npm 配置(`@marktrue:registry` 优先,未设置时用默认 `registry`),手机不直连 npm,registry 凭据不下发。
10
+ - 稳定版跟随 `latest`;预发布跟随自身通道(`alpha` / `rc`);carrier 使用精确 `<插件完整版本>-app` 并由 `app` tag 约束。
11
+ - 校验 npm `dist.integrity`、插件内容哈希、载体清单、APK 身份与签名;缺 carrier、元数据不一致、降级一律 fail-closed,不改 profile、不触发重启。
12
+ - profile 依赖保持精确 npm 版本,不保留长期 `file:` tarball;回滚恢复更新前精确 spec。
13
+ - 旧 carrier 缺轻量元数据时:仅用户显式操作后准备并校验 carrier,先展示真实 App build,再要求最终确认;最终 POST 前不改 profile。
14
+
15
+ ### 手机 / 桌面
16
+
17
+ - 移除 App 更新源菜单;App-only 入口改名「检查 App 更新」。
18
+ - 插件更新详情与安装日志合并为可折叠、可复制的脱敏面板,按 opId + 序号回放,重启后仍可查看。
19
+ - 新增「更新进度」卡片:六步时间线、当前阶段、已用时、目标版本、最近一行 pnpm 输出;失败直接显示可读原因与稳定码。
20
+ - 完成后等待宿主固定 App 发布就绪再显示完成;「检查 App 更新」自动接续,无需回首页刷新。
21
+ - 历史失败/未确认事务不再阻塞 App 更新;APK 按 sha 复用,安装未完成给出可执行指引。
22
+ - 桌面「连接移动端」同步显示阶段文本与最新输出。
23
+
24
+ ### 平台
25
+
26
+ - Windows:npm/pnpm 经 shell 调用,缺失客户端给出稳定原因;桌面原生自更新仍受 `restart-unsupported` 限制(Linux/WSL 宿主可完成自更新)。
27
+
3
28
  ## v3.2.1-alpha.3(2026-10-26)— Android release 签名密钥迁移
4
29
 
5
30
  > 插件 `3.2.1-alpha.3` / App `3.2.1+25` / 载体 `3.2.1-alpha.3-app`。插件更新 `alpha` dist-tag;`latest` 保持在 alpha1。
@@ -391,6 +391,13 @@ function validateCarrierCandidate(candidate, plugin) {
391
391
  if (!manifestFile.ok) return { ok: false, reason: manifestFile.code === "missing" ? "carrier-manifest-missing" : "carrier-manifest-unreadable" };
392
392
  const problem = manifestProblem(manifestFile.value, plugin);
393
393
  if (problem) return { ok: false, reason: problem };
394
+ // New carriers expose this in NPM packument for lightweight checks; old carriers may omit it.
395
+ if (pkg.appVersion !== undefined && pkg.appVersion !== manifestFile.value.app.version) {
396
+ return { ok: false, reason: "carrier-app-version-metadata-mismatch" };
397
+ }
398
+ if (pkg.pluginContentSha256 !== undefined && pkg.pluginContentSha256 !== manifestFile.value.plugin.contentSha256) {
399
+ return { ok: false, reason: "carrier-plugin-content-sha256-metadata-mismatch" };
400
+ }
394
401
  const apk = validateCarrierApk(candidate.source, manifestFile.value.app);
395
402
  if (!apk.ok) return { ok: false, reason: apk.reason };
396
403
  let contentSha256 = null;
package/lib/client.js CHANGED
@@ -331,6 +331,153 @@ window.__ModuleLoader__.load({
331
331
  }
332
332
  exports.buildSectionModel = buildSectionModel;
333
333
 
334
+ /** Normalize the host's authenticated NPM check response for the settings panel. */
335
+ function buildPluginUpdateModel(status) {
336
+ var value = status && typeof status === "object" ? status : {};
337
+ var candidate = value.candidate && typeof value.candidate === "object" ? value.candidate : null;
338
+ var plugin = candidate && candidate.plugin && typeof candidate.plugin === "object" ? candidate.plugin : {};
339
+ var carrier = candidate && candidate.carrier && typeof candidate.carrier === "object" ? candidate.carrier : {};
340
+ var state = String(value.state || "unknown");
341
+ var transaction = value.transaction && typeof value.transaction === "object" ? value.transaction : null;
342
+ var appVersion = String(carrier.appVersion || transaction?.targetAppVersion || "");
343
+ return {
344
+ state: state,
345
+ reason: String(value.reason || ""),
346
+ candidate: candidate,
347
+ hasUpdate: state === "update-available" && Boolean(plugin.version),
348
+ canSelfUpdate: value.canSelfUpdate === true,
349
+ pluginVersion: String(plugin.version || transaction?.targetVersion || ""),
350
+ appVersion: appVersion,
351
+ needsPreparation: state === "update-available" && !appVersion,
352
+ transaction: value.transaction && typeof value.transaction === "object" ? value.transaction : null,
353
+ guidance: value.guidance && typeof value.guidance === "object" ? value.guidance : null,
354
+ checkLog: pluginUpdateCheckLogLines(value),
355
+ };
356
+ }
357
+ exports.buildPluginUpdateModel = buildPluginUpdateModel;
358
+
359
+ /** 检查日志投影:只保留有界字符串行,桌面与手机显示同一份脱敏诊断。 */
360
+ function pluginUpdateCheckLogLines(value) {
361
+ var source = value && typeof value === "object" && Array.isArray(value.checkLog) ? value.checkLog : [];
362
+ return source
363
+ .map(function (line) { return String(line == null ? "" : line).trim(); })
364
+ .filter(function (line) { return line !== ""; })
365
+ .slice(-40);
366
+ }
367
+ exports.pluginUpdateCheckLogLines = pluginUpdateCheckLogLines;
368
+
369
+ /** pnpm/宿主 stage → 面向用户的阶段名(与手机端一致)。 */
370
+ function pluginUpdateStageText(stage) {
371
+ var value = String(stage || "").toLowerCase();
372
+ var known = {
373
+ staging: "准备更新", prepare: "准备更新", download: "下载插件与载体", verify: "校验下载产物",
374
+ extract: "校验插件内容", snapshot: "生成安装快照", switch: "写入 profile 依赖",
375
+ install: "安装依赖(pnpm install)", restart: "重启宿主", "restart-pending": "等待宿主重启",
376
+ supervisor: "等待健康确认", worker: "电脑端工人进程处理中", finished: "更新完成", failed: "更新失败",
377
+ };
378
+ return known[value] || String(stage || "");
379
+ }
380
+ exports.pluginUpdateStageText = pluginUpdateStageText;
381
+
382
+ var PLUGIN_UPDATE_STEPS = ["检查候选", "下载校验", "安装依赖", "重启宿主", "健康确认", "完成"];
383
+
384
+ /** 事务 + 最新日志事件 → 六步进度(桌面与手机端同一套语义)。 */
385
+ function pluginUpdateProgress(model, lastEvent) {
386
+ var transaction = model && model.transaction && typeof model.transaction === "object" ? model.transaction : null;
387
+ var phase = String(transaction && transaction.phase || "");
388
+ var result = transaction && transaction.result && typeof transaction.result === "object" ? transaction.result : null;
389
+ var rolledBack = Boolean(transaction && transaction.rolledBack === true);
390
+ var failed = rolledBack || phase === "rolled-back" || phase === "failed" || (phase === "settled" && result && result.ok === false);
391
+ var terminal = ["settled", "failed", "rolled-back"].includes(phase);
392
+ if (failed) return { step: 4, text: "更新失败/已回滚", terminal: true, failed: true };
393
+ if (phase === "settled") return { step: 5, text: "更新完成", terminal: true, failed: false };
394
+ if (phase === "rolling-back") return { step: 4, text: "健康确认失败,正在回滚", terminal: false, failed: true };
395
+ var stage = String(lastEvent && lastEvent.stage || "").toLowerCase();
396
+ var byStage = {
397
+ staging: 0, prepare: 0, download: 1, verify: 1, extract: 1, snapshot: 1,
398
+ switch: 2, install: 2, worker: 2, restart: 3, "restart-pending": 3,
399
+ supervisor: 4, finished: 5, failed: 4,
400
+ };
401
+ if (byStage[stage] !== undefined) return { step: byStage[stage], text: pluginUpdateStageText(stage) || "处理中", terminal: false, failed: false };
402
+ var byPhase = { staging: 0, installing: 2, "restart-pending": 3 };
403
+ if (byPhase[phase] !== undefined) return { step: byPhase[phase], text: pluginUpdateProgressPhaseText(phase), terminal: false, failed: false };
404
+ return { step: 0, text: "准备中", terminal: false, failed: false };
405
+ }
406
+ function pluginUpdateProgressPhaseText(phase) {
407
+ if (phase === "staging") return "准备更新";
408
+ if (phase === "installing") return "安装依赖(pnpm install)";
409
+ if (phase === "restart-pending") return "等待宿主重启";
410
+ return pluginUpdateStageText(phase) || "处理中";
411
+ }
412
+ exports.pluginUpdateProgress = pluginUpdateProgress;
413
+ exports.pluginUpdateProgressSteps = PLUGIN_UPDATE_STEPS;
414
+
415
+ /** 安装进度事件 → 一行文本(与手机端一致)。 */
416
+ function pluginUpdateEventLine(event) {
417
+ var stage = String(event && event.stage || "update");
418
+ var stream = String(event && event.stream || "status");
419
+ var text = String(event && event.text || "");
420
+ return stream === "status" ? "[" + stage + "] " + text : "[" + stage + "][" + stream + "] " + text;
421
+ }
422
+
423
+ /** 失败原因的稳定码 → 可读文案(未知码原样显示,绝不隐藏)。 */
424
+ function updateFailureReasonText(reason) {
425
+ var value = String(reason || "");
426
+ var known = {
427
+ "npm-carrier-plugin-identity-mismatch": "载体清单与 NPM 包身份不一致(版本/大小校验失败)",
428
+ "npm-carrier-manifest-unreadable": "载体清单不可读",
429
+ "npm-carrier-extract-failed": "载体解包失败",
430
+ "npm-plugin-tarball-sha256-mismatch": "插件 tarball 字节哈希与载体清单不一致",
431
+ "npm-plugin-content-sha256-mismatch": "插件内容标识与载体清单不一致",
432
+ "npm-packument-content-sha256-mismatch": "NPM 包内容与载体清单不一致",
433
+ "npm-packument-app-version-mismatch": "NPM 包 App build 与载体清单不一致",
434
+ "npm-pack-failed": "宿主机 npm pack 下载插件/载体失败",
435
+ "npm-pack-integrity-mismatch": "npm pack 产物完整性不符(registry 返回异常)",
436
+ "npm-pack-metadata-mismatch": "npm pack 返回的包版本/完整性不符",
437
+ "npm-pack-tarball-missing": "npm pack 产物缺失",
438
+ "install-failed": "pnpm 安装失败",
439
+ "health-check-failed": "新插件健康检查失败,已回滚",
440
+ "rollback-failed": "回滚失败,需要人工处理",
441
+ "restart-unsupported": "宿主形态不支持自动重启",
442
+ };
443
+ return known[value] ? known[value] + "(" + value + ")" : value;
444
+ }
445
+ exports.updateFailureReasonText = updateFailureReasonText;
446
+
447
+ /** 合并检查详情与安装详情:一个“插件更新详情”列表,桌面/手机同源。 */
448
+ function pluginUpdateDetailLines(model, events) {
449
+ var lines = pluginUpdateCheckLogLines(model).slice();
450
+ var install = (Array.isArray(events) ? events : []).map(pluginUpdateEventLine).filter(Boolean);
451
+ if (install.length) {
452
+ lines.push("# 安装详情(宿主 pnpm 输出,已脱敏)");
453
+ lines = lines.concat(install);
454
+ }
455
+ var transaction = model && model.transaction && typeof model.transaction === "object" ? model.transaction : null;
456
+ var result = transaction && transaction.result && typeof transaction.result === "object" ? transaction.result : null;
457
+ if (result && result.reason) {
458
+ lines.push("# 安装结果:" + String(transaction.phase || "failed") + " · 原因:" + updateFailureReasonText(result.reason));
459
+ }
460
+ return lines;
461
+ }
462
+ exports.pluginUpdateDetailLines = pluginUpdateDetailLines;
463
+
464
+ /** 检查不可用的可读原因:区分「npm 客户端缺失」与「registry 不可用」。 */
465
+ function updateCheckReasonText(reason) {
466
+ if (reason === "npm-client-unavailable") return "找不到 npm 客户端(请确认 DSH 进程的 PATH 包含 npm)";
467
+ if (reason === "npm-registry-unavailable") return "无法访问 npm registry(请检查主机 npm 配置与网络)";
468
+ if (reason === "npm-channel-version-missing") return "npm 上没有本机预发布通道对应的 dist-tag(展开检查日志查看查询步骤)";
469
+ if (reason === "npm-plugin-metadata-unavailable") return "读取插件版本元数据失败(展开检查日志查看查询步骤)";
470
+ if (reason === "npm-carrier-metadata-unavailable") return "读取配对载体元数据失败(展开检查日志查看查询步骤)";
471
+ return reason || "registry-unavailable";
472
+ }
473
+ exports.updateCheckReasonText = updateCheckReasonText;
474
+
475
+ function apiRoute(basePath, route) {
476
+ var mount = String(basePath || "/m").replace(/\/+$/, "");
477
+ if (!mount || mount === "/") mount = "/m";
478
+ return mount + "/api" + route;
479
+ }
480
+
334
481
  /** 选项卡按钮(内联样式,与设置页现有观感一致)。 */
335
482
  function tabButton(tab, active, onSelect) {
336
483
  return jsxRuntime.jsx(
@@ -578,6 +725,112 @@ window.__ModuleLoader__.load({
578
725
  });
579
726
  }
580
727
 
728
+ function pluginUpdatePane(model, workflow, logs, logExpanded, showAllLogs, actions) {
729
+ var candidate = workflow.candidate || model.candidate || {};
730
+ var plugin = candidate.plugin && typeof candidate.plugin === "object" ? candidate.plugin : {};
731
+ var carrier = candidate.carrier && typeof candidate.carrier === "object" ? candidate.carrier : {};
732
+ var transaction = model.transaction && typeof model.transaction === "object" ? model.transaction : {};
733
+ var pluginVersion = String(workflow.transaction && transaction.targetVersion || plugin.version || model.pluginVersion || "");
734
+ var appVersion = String(workflow.transaction && transaction.targetAppVersion || carrier.appVersion || model.appVersion || "");
735
+ var events = Array.isArray(logs.events) ? logs.events : [];
736
+ var installLines = events.map(pluginUpdateEventLine).filter(Boolean);
737
+ var visibleInstallLines = installLines;
738
+ if (!showAllLogs && installLines.length > 12) {
739
+ visibleInstallLines = installLines.slice(0, 4).concat(["… 中间 " + (installLines.length - 12) + " 行已折叠 …"], installLines.slice(-8));
740
+ }
741
+ var checkLines = pluginUpdateCheckLogLines(model);
742
+ var displayLines = checkLines.concat(installLines.length ? ["# 安装详情(宿主 pnpm 输出,已脱敏)"] : [], visibleInstallLines);
743
+ var detailLineCount = pluginUpdateDetailLines(model, events).length;
744
+ var working = ["preparing", "submitting", "running"].includes(workflow.status);
745
+ var statusText = model.state === "loading" ? "正在检查 NPM 更新…"
746
+ : model.state === "update-available" ? "发现插件更新候选"
747
+ : model.state === "up-to-date" || model.state === "local-ahead" ? "当前插件已是最新"
748
+ : model.state === "disabled" ? "NPM 自动更新已关闭"
749
+ : model.state === "unavailable" ? "检查暂不可用:" + updateCheckReasonText(model.reason)
750
+ : "等待检查";
751
+ var operationTarget = workflow.transaction && (pluginVersion || appVersion)
752
+ ? jsxRuntime.jsx("div", { style: { fontSize: "12px", lineHeight: "1.6" }, children: "操作目标:插件 v" + (pluginVersion || "未知") + (appVersion ? " · App build " + appVersion : "") })
753
+ : null;
754
+ var actionButton = model.hasUpdate && model.canSelfUpdate && !["preparing", "submitting", "running", "confirming"].includes(workflow.status)
755
+ ? jsxRuntime.jsx("button", {
756
+ style: Object.assign({}, btnStyle, { marginLeft: 0 }),
757
+ disabled: working,
758
+ onClick: actions.onBegin,
759
+ children: model.needsPreparation ? "准备更新" : "更新插件",
760
+ })
761
+ : null;
762
+ var confirmBox = workflow.status === "confirming"
763
+ ? jsxRuntime.jsx("div", {
764
+ style: { padding: "10px", borderRadius: "8px", border: "1px solid var(--dsw-alias-brand-primary, #4d6bfe)", background: "rgba(77,107,254,.08)" },
765
+ children: [
766
+ jsxRuntime.jsx("div", { style: { fontWeight: 600, marginBottom: "4px" }, children: "请确认安装目标" }),
767
+ jsxRuntime.jsx("div", { style: { fontSize: "12.5px", lineHeight: "1.6" }, children: "插件 v" + pluginVersion + (appVersion ? " · App build " + appVersion : "") }),
768
+ jsxRuntime.jsx("div", { style: { fontSize: "12px", margin: "4px 0 8px", color: "var(--dsw-alias-label-secondary, #888)" }, children: "确认后电脑端会使用主机 npm 配置安装,并重启 DSH;连接短暂中断属正常。" }),
769
+ jsxRuntime.jsx("div", { style: { display: "flex", gap: "8px" }, children: [
770
+ jsxRuntime.jsx("button", { style: Object.assign({}, btnStyle, { marginLeft: 0 }), onClick: actions.onCancel, children: "取消" }),
771
+ jsxRuntime.jsx("button", { style: Object.assign({}, btnStyle, { marginLeft: 0, borderColor: "var(--dsw-alias-brand-primary, #4d6bfe)" }), onClick: actions.onConfirm, children: "确认并安装" }),
772
+ ] }),
773
+ ],
774
+ })
775
+ : null;
776
+ var lastEvent = events.length ? events[events.length - 1] : null;
777
+ var progress = pluginUpdateProgress(model, lastEvent);
778
+ var liveLine = lastEvent && lastEvent.text ? String(lastEvent.text).trim() : "";
779
+ if (liveLine.length > 140) liveLine = liveLine.slice(0, 140) + "…";
780
+ var progressSteps = PLUGIN_UPDATE_STEPS.map(function (name, index) {
781
+ var done = index <= progress.step;
782
+ return jsxRuntime.jsx("span", { style: { color: done ? (progress.failed ? "var(--dsw-alias-text-danger, #e5484d)" : "var(--dsw-alias-brand-primary, #4d6bfe)") : "var(--dsw-alias-label-tertiary, #9aa0a6)", fontWeight: index === progress.step ? 700 : 400, marginRight: "6px" }, children: (done ? "● " : "○ ") + name }, "update-step-" + index);
783
+ });
784
+ var progressBox = (workflow.transaction || model.transaction || events.length)
785
+ ? jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: "3px" }, children: [
786
+ jsxRuntime.jsx("div", { style: { fontSize: "12px", fontWeight: 600 }, children: "当前阶段:" + progress.text }),
787
+ jsxRuntime.jsx("div", { style: { fontSize: "10.5px", display: "flex", flexWrap: "wrap" }, children: progressSteps }),
788
+ liveLine ? jsxRuntime.jsx("div", { style: { fontSize: "11px", fontFamily: "monospace", color: "var(--dsw-alias-label-secondary, #888)", whiteSpace: "pre-wrap", overflowWrap: "anywhere" }, children: "最新输出:" + liveLine }) : null,
789
+ ] })
790
+ : null;
791
+ var checkLog = Array.isArray(model.checkLog) ? model.checkLog.slice(-40) : [];
792
+ var detailsVisible = detailLineCount > 0 || workflow.transaction || events.length;
793
+ var detailsPanel = detailsVisible
794
+ ? jsxRuntime.jsx("div", { style: { marginTop: "4px", display: "flex", flexDirection: "column", gap: "8px" }, children: [
795
+ jsxRuntime.jsx("button", { style: Object.assign({}, btnStyle, { marginLeft: 0, alignSelf: "flex-start" }), "aria-expanded": !!logExpanded, onClick: actions.onToggleLogs,
796
+ children: (logExpanded ? "收起" : "查看") + "插件更新详情(" + detailLineCount + " 行)" }),
797
+ logExpanded ? jsxRuntime.jsx("div", { style: { border: "1px solid var(--dsw-alias-divider-strong, rgba(128,128,128,.2))", borderRadius: "8px", overflow: "hidden" }, children: displayLines.length ? displayLines.map(function (line, index) {
798
+ var command = line.charAt(0) === "$";
799
+ var note = line.charAt(0) === "#";
800
+ var failed = /^\[(failed|rolled-back|rollback)\]/.test(line);
801
+ if (command) {
802
+ return jsxRuntime.jsx("div", { style: { display: "flex", alignItems: "center", gap: "8px", padding: "6px 10px", background: "rgba(0,0,0,.05)", fontFamily: "monospace", fontSize: "11.5px" }, children: [
803
+ jsxRuntime.jsx("span", { style: { color: "#1f9d55", lineHeight: 1 }, children: "●" }),
804
+ jsxRuntime.jsx("span", { style: { flex: 1, whiteSpace: "pre-wrap", overflowWrap: "anywhere" }, children: line }),
805
+ jsxRuntime.jsx("button", { style: Object.assign({}, btnStyle, { marginLeft: 0, padding: "1px 8px", fontSize: "11px" }), onClick: actions.onCopyDetails, children: "复制" }),
806
+ ] }, "update-details-cmd-" + index);
807
+ }
808
+ return jsxRuntime.jsx("div", { style: { padding: "2px 10px", fontFamily: "monospace", fontSize: "11px", lineHeight: "1.55", whiteSpace: "pre-wrap", overflowWrap: "anywhere", color: failed ? "var(--dsw-alias-text-danger, #e5484d)" : note ? "var(--dsw-alias-label-tertiary, #9aa0a6)" : "inherit" }, children: line }, "update-details-line-" + index);
809
+ }) : jsxRuntime.jsx("div", { style: { padding: "6px 10px", fontFamily: "monospace", fontSize: "11px", color: "var(--dsw-alias-label-secondary, #888)" }, children: "等待 pnpm 输出…" }) }) : null,
810
+ logExpanded && installLines.length > 12 ? jsxRuntime.jsx("button", { style: Object.assign({}, btnStyle, { marginLeft: 0, alignSelf: "flex-start" }), onClick: actions.onToggleAllLogs, children: showAllLogs ? "折叠中间安装日志" : "展开全部安装日志" }) : null,
811
+ ] })
812
+ : null;
813
+ return jsxRuntime.jsx("div", {
814
+ style: { marginTop: "14px", padding: "12px", borderRadius: "10px", border: "1px solid var(--dsw-alias-divider-strong, rgba(128,128,128,.2))", display: "flex", flexDirection: "column", gap: "8px" },
815
+ children: [
816
+ jsxRuntime.jsx("div", { style: { fontSize: "14px", fontWeight: 650 }, children: "电脑端插件更新" }),
817
+ jsxRuntime.jsx("div", { style: { fontSize: "12.5px", color: "var(--dsw-alias-label-secondary, #888)" }, children: statusText }),
818
+ progressBox,
819
+ model.hasUpdate ? jsxRuntime.jsx("div", { style: { fontSize: "12.5px", lineHeight: "1.6" }, children: "插件 v" + pluginVersion + (appVersion ? " · App build " + appVersion : " · App build 将在准备后核验") }) : null,
820
+ operationTarget,
821
+ model.hasUpdate && !model.canSelfUpdate ? jsxRuntime.jsx("div", { style: { fontSize: "12px", color: "var(--dsw-alias-label-secondary, #888)" }, children: model.guidance?.summary || "当前安装形态需要按电脑端指引更新。" }) : null,
822
+ workflow.status === "preparing" ? jsxRuntime.jsx("div", { style: { fontSize: "12px" }, children: "正在从主机 NPM 源准备并校验 carrier;profile 尚未修改…" }) : null,
823
+ workflow.status === "submitting" ? jsxRuntime.jsx("div", { style: { fontSize: "12px" }, children: "正在提交安装事务…" }) : null,
824
+ workflow.status === "running" ? jsxRuntime.jsx("div", { style: { fontSize: "12px" }, children: "pnpm 安装/重启进行中;输出会保留并可在重连后继续查看。" }) : null,
825
+ workflow.message ? jsxRuntime.jsx("div", { style: { fontSize: "12px", color: workflow.status === "failed" ? "var(--dsw-alias-text-danger, #e5484d)" : "inherit" }, children: workflow.message }) : null,
826
+ detailsPanel,
827
+ actionButton,
828
+ confirmBox,
829
+ jsxRuntime.jsx("button", { style: Object.assign({}, btnStyle, { marginLeft: 0, alignSelf: "flex-start" }), disabled: model.state === "loading", onClick: actions.onCheck, children: model.state === "loading" ? "检查中…" : "手动检查更新" }),
830
+ ],
831
+ });
832
+ }
833
+
581
834
  /**
582
835
  * 设置页 section:连接移动端设备(两个选项卡:已安装 App / 未安装 App)。
583
836
  * 挂载后从 /m/api/qr-config 拉取地址、口令与下载码数据。
@@ -597,8 +850,38 @@ window.__ModuleLoader__.load({
597
850
  var refreshState = useState(0);
598
851
  var refreshTick = refreshState[0];
599
852
  var setRefreshTick = refreshState[1];
853
+ var updateCheckState = useState({ state: "loading", reason: "not-checked", hasUpdate: false, canSelfUpdate: false, candidate: null });
854
+ var updateStatus = updateCheckState[0];
855
+ var setUpdateStatus = updateCheckState[1];
856
+ var updateTickState = useState(0);
857
+ var updateTick = updateTickState[0];
858
+ var setUpdateTick = updateTickState[1];
859
+ var workflowState = useState({ status: "idle", opId: "", prepareId: "", candidate: null, transaction: false, message: "" });
860
+ var workflow = workflowState[0];
861
+ var setWorkflow = workflowState[1];
862
+ var logsState = useState({ events: [], lastSeq: 0, firstSeq: 0, truncated: false });
863
+ var logs = logsState[0];
864
+ var setLogs = logsState[1];
865
+ var logExpandedState = useState(false);
866
+ var logExpanded = logExpandedState[0];
867
+ var setLogExpanded = logExpandedState[1];
868
+ var showAllLogsState = useState(false);
869
+ var showAllLogs = showAllLogsState[0];
870
+ var setShowAllLogs = showAllLogsState[1];
871
+ // 检查日志展开状态与复制:桌面端与手机端使用同一份宿主 checkLog。
600
872
  var requestGeneration = 0;
601
873
 
874
+ var copyUpdateDetails = function () {
875
+ var lines = pluginUpdateDetailLines(updateStatus, logs.events);
876
+ if (!lines.length) return;
877
+ var text = lines.join("\n");
878
+ try {
879
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
880
+ navigator.clipboard.writeText(text);
881
+ }
882
+ } catch (_) { /* 复制失败不影响日志展示 */ }
883
+ };
884
+
602
885
  useEffect(function () {
603
886
  var alive = true;
604
887
  var timer = null;
@@ -655,9 +938,185 @@ window.__ModuleLoader__.load({
655
938
  if (timer) clearTimeout(timer);
656
939
  };
657
940
  }, [refreshTick]);
941
+
942
+ // Automatically check the host's NPM candidate when the connection token becomes available.
943
+ useEffect(function () {
944
+ if (data.status !== "ready" || !data.token) {
945
+ if (data.status === "ready") setUpdateStatus({ state: "unavailable", reason: "auth-token-unavailable", hasUpdate: false, canSelfUpdate: false, candidate: null });
946
+ return;
947
+ }
948
+ var alive = true;
949
+ setUpdateStatus(function (previous) { return Object.assign({}, previous, { state: "loading", reason: "checking" }); });
950
+ fetch(apiRoute(data.basePath, "/plugin-update/check"), {
951
+ method: "GET",
952
+ headers: { accept: "application/json", "x-mobile-token": data.token },
953
+ })
954
+ .then(function (res) { if (!res.ok) throw new Error("HTTP " + res.status); return res.json(); })
955
+ .then(function (body) {
956
+ if (!alive) return;
957
+ var model = buildPluginUpdateModel(body.pluginUpdate);
958
+ setUpdateStatus(model);
959
+ var tx = model.transaction;
960
+ if (tx && tx.opId) {
961
+ var terminal = ["settled", "rolled-back", "failed"].includes(String(tx.phase));
962
+ if (!terminal) setLogExpanded(true);
963
+ setWorkflow(function (previous) {
964
+ if (previous.opId === tx.opId && previous.transaction) return previous;
965
+ return { status: terminal ? (tx.phase === "settled" ? "done" : "failed") : "running",
966
+ opId: tx.opId, prepareId: "", candidate: model.candidate, transaction: true,
967
+ message: terminal ? (tx.phase === "settled" ? "最近一次插件更新已完成。" : "最近一次插件更新结束:" + tx.phase) : "接续电脑端正在进行的插件更新。" };
968
+ });
969
+ }
970
+ })
971
+ .catch(function (err) {
972
+ if (alive) setUpdateStatus({ state: "unavailable", reason: String(err?.message ?? "update-check-failed"), hasUpdate: false, canSelfUpdate: false, candidate: null });
973
+ });
974
+ return function () { alive = false; };
975
+ }, [data.status, data.basePath, data.token, updateTick]);
976
+
658
977
  var refresh = function () {
659
978
  setRefreshTick(function (n) { return n + 1; });
660
979
  };
980
+ var checkPluginUpdate = function () { setUpdateTick(function (n) { return n + 1; }); };
981
+ var makeOperationId = function () {
982
+ if (window.crypto && typeof window.crypto.randomUUID === "function") return window.crypto.randomUUID();
983
+ return "web-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 12);
984
+ };
985
+ var postUpdateJson = function (route, payload) {
986
+ return fetch(apiRoute(data.basePath, route), {
987
+ method: "POST",
988
+ headers: { accept: "application/json", "content-type": "application/json", "x-mobile-token": data.token },
989
+ body: JSON.stringify(payload),
990
+ }).then(function (res) {
991
+ return res.json().catch(function () { return {}; }).then(function (body) {
992
+ if (!res.ok) {
993
+ var error = new Error(String(body.error || ("HTTP " + res.status)));
994
+ error.code = String(body.error || ("http-" + res.status));
995
+ error.status = res.status;
996
+ error.body = body;
997
+ throw error;
998
+ }
999
+ return body;
1000
+ });
1001
+ });
1002
+ };
1003
+ var beginPluginUpdate = function () {
1004
+ var candidate = updateStatus.candidate;
1005
+ if (!updateStatus.hasUpdate || !candidate) return;
1006
+ var opId = makeOperationId();
1007
+ setLogs({ events: [], lastSeq: 0, firstSeq: 0, truncated: false });
1008
+ setWorkflow({ status: updateStatus.needsPreparation ? "preparing" : "confirming", opId: opId,
1009
+ prepareId: "", candidate: candidate, transaction: false, message: "" });
1010
+ if (!updateStatus.needsPreparation) return;
1011
+ postUpdateJson("/plugin-update/prepare", { opId: opId })
1012
+ .then(function (body) {
1013
+ if (body.action === "prepared" && body.candidate && body.prepareId) {
1014
+ var preparedModel = buildPluginUpdateModel({ state: "update-available", canSelfUpdate: true,
1015
+ candidate: body.candidate, transaction: updateStatus.transaction });
1016
+ setUpdateStatus(preparedModel);
1017
+ setWorkflow({ status: "confirming", opId: opId, prepareId: body.prepareId,
1018
+ candidate: body.candidate, transaction: false, message: "Carrier 已验证,请确认实际 App build 后继续。" });
1019
+ } else if (body.action === "confirmation-required") {
1020
+ setWorkflow({ status: "confirming", opId: opId, prepareId: "", candidate: body.candidate || candidate,
1021
+ transaction: false, message: "请确认更新目标后继续。" });
1022
+ } else {
1023
+ setWorkflow({ status: "failed", opId: opId, prepareId: "", candidate: candidate, transaction: false,
1024
+ message: "无法准备更新:" + String(body.error || body.action || "no-update-candidate") });
1025
+ }
1026
+ })
1027
+ .catch(function (err) {
1028
+ var tx = err.body && err.body.transaction && typeof err.body.transaction === "object" ? err.body.transaction : null;
1029
+ if (tx && typeof tx.opId === "string" && tx.opId) {
1030
+ setLogs({ events: [], lastSeq: 0, firstSeq: 0, truncated: false });
1031
+ setUpdateStatus(buildPluginUpdateModel({ state: "up-to-date", canSelfUpdate: false, candidate: null, transaction: tx }));
1032
+ setWorkflow({ status: "running", opId: tx.opId, prepareId: "", candidate: null,
1033
+ transaction: true, message: "电脑端已有另一客户端启动的更新,接续查看同一事务日志。" });
1034
+ setLogExpanded(true);
1035
+ checkPluginUpdate();
1036
+ return;
1037
+ }
1038
+ setWorkflow({ status: "failed", opId: opId, prepareId: "", candidate: candidate, transaction: false,
1039
+ message: "Carrier 预检失败:" + String(err.code || "npm-preparation-failed") });
1040
+ });
1041
+ };
1042
+ var confirmPluginUpdate = function () {
1043
+ if (!workflow.opId || !workflow.candidate) return;
1044
+ var plugin = workflow.candidate.plugin || {};
1045
+ var payload = { opId: workflow.opId, targetVersion: String(plugin.version || "") };
1046
+ if (workflow.prepareId) payload.prepareId = workflow.prepareId;
1047
+ setWorkflow(function (previous) { return Object.assign({}, previous, { status: "submitting", message: "正在提交电脑端安装事务…" }); });
1048
+ postUpdateJson("/plugin-update", payload)
1049
+ .then(function (body) {
1050
+ if (body.action === "accepted") {
1051
+ setWorkflow(function (previous) { return Object.assign({}, previous, { status: "running", transaction: true, message: "安装/重启进行中;日志会在重连后继续显示。" }); });
1052
+ setLogExpanded(true);
1053
+ checkPluginUpdate();
1054
+ } else if (body.action === "prepare-required") {
1055
+ setWorkflow(function (previous) { return Object.assign({}, previous, { status: "failed", transaction: false, message: "更新目标需要重新准备;请再次点击准备更新。" }); });
1056
+ checkPluginUpdate();
1057
+ } else if (body.action === "none") {
1058
+ setWorkflow(function (previous) { return Object.assign({}, previous, { status: "idle", transaction: false, message: "检查后未发现可安装的新版本。" }); });
1059
+ checkPluginUpdate();
1060
+ } else {
1061
+ setWorkflow(function (previous) { return Object.assign({}, previous, { status: "failed", transaction: false, message: "更新未接受:" + String(body.error || body.action || "unknown") }); });
1062
+ }
1063
+ })
1064
+ .catch(function (err) {
1065
+ var tx = err.body && err.body.transaction && typeof err.body.transaction === "object" ? err.body.transaction : null;
1066
+ if (tx && typeof tx.opId === "string" && tx.opId) {
1067
+ var terminal = ["settled", "rolled-back", "failed"].includes(String(tx.phase));
1068
+ setLogs({ events: [], lastSeq: 0, firstSeq: 0, truncated: false });
1069
+ setUpdateStatus(buildPluginUpdateModel({ state: "up-to-date", canSelfUpdate: false, candidate: null, transaction: tx }));
1070
+ setWorkflow({ status: terminal ? (tx.phase === "settled" ? "done" : "failed") : "running",
1071
+ opId: tx.opId, prepareId: "", candidate: null, transaction: true,
1072
+ message: "电脑端已有另一客户端启动的更新,接续查看同一事务日志。" });
1073
+ setLogExpanded(true);
1074
+ checkPluginUpdate();
1075
+ return;
1076
+ }
1077
+ setWorkflow(function (previous) { return Object.assign({}, previous, { status: "failed", transaction: false, message: "更新请求失败:" + String(err.code || "request-failed") }); });
1078
+ if (err.code === "update-busy") checkPluginUpdate();
1079
+ });
1080
+ };
1081
+ var cancelPluginUpdateConfirm = function () {
1082
+ setWorkflow({ status: "idle", opId: "", prepareId: "", candidate: null, transaction: false, message: "" });
1083
+ };
1084
+
1085
+ useEffect(function () {
1086
+ if (data.status !== "ready" || !data.token || !workflow.opId || !workflow.transaction) return;
1087
+ var alive = true;
1088
+ var timer = null;
1089
+ var cursor = 0;
1090
+ var poll = function () {
1091
+ var route = apiRoute(data.basePath, "/plugin-update/log") + "?opId=" + encodeURIComponent(workflow.opId) + "&afterSeq=" + cursor;
1092
+ fetch(route, { method: "GET", headers: { accept: "application/json", "x-mobile-token": data.token } })
1093
+ .then(function (res) { if (!res.ok) throw new Error("HTTP " + res.status); return res.json(); })
1094
+ .then(function (body) {
1095
+ if (!alive) return;
1096
+ var events = Array.isArray(body.events) ? body.events : [];
1097
+ if (Number.isSafeInteger(body.lastSeq)) cursor = Math.max(cursor, body.lastSeq);
1098
+ if (events.length) {
1099
+ setLogs(function (previous) {
1100
+ var bySeq = new Map((previous.events || []).map(function (event) { return [event.seq, event]; }));
1101
+ events.forEach(function (event) { bySeq.set(event.seq, event); });
1102
+ var merged = Array.from(bySeq.values()).sort(function (a, b) { return a.seq - b.seq; }).slice(-400);
1103
+ return { events: merged, lastSeq: cursor, firstSeq: body.firstSeq || previous.firstSeq || 0, truncated: Boolean(body.truncated) };
1104
+ });
1105
+ }
1106
+ if (["settled", "rolled-back", "failed"].includes(String(body.phase))) {
1107
+ setWorkflow(function (previous) { return Object.assign({}, previous, { status: body.phase === "settled" ? "done" : "failed", message: body.phase === "settled" ? "电脑端插件更新完成。" : "电脑端插件更新结束:" + body.phase }); });
1108
+ return;
1109
+ }
1110
+ timer = setTimeout(poll, 1000);
1111
+ })
1112
+ .catch(function () { if (alive) timer = setTimeout(poll, 1500); });
1113
+ };
1114
+ poll();
1115
+ return function () { alive = false; if (timer) clearTimeout(timer); };
1116
+ }, [data.status, data.basePath, data.token, workflow.opId, workflow.transaction]);
1117
+
1118
+ var toggleLog = function () { setLogExpanded(function (open) { return !open; }); };
1119
+ var toggleAllLogs = function () { setShowAllLogs(function (open) { return !open; }); };
661
1120
 
662
1121
  if (data.status === "loading") {
663
1122
  return jsxRuntime.jsx("div", { style: { padding: "20px 0", color: "var(--dsw-alias-label-secondary, #888)" }, children: "加载中…" });
@@ -681,6 +1140,15 @@ window.__ModuleLoader__.load({
681
1140
  }),
682
1141
  }),
683
1142
  activeIsDownload ? downloadPane(model, refresh) : connectionPane(model, data, refresh, data.retryExhausted === true),
1143
+ pluginUpdatePane(updateStatus, workflow, logs, logExpanded, showAllLogs, {
1144
+ onCheck: checkPluginUpdate,
1145
+ onBegin: beginPluginUpdate,
1146
+ onConfirm: confirmPluginUpdate,
1147
+ onCancel: cancelPluginUpdateConfirm,
1148
+ onToggleLogs: toggleLog,
1149
+ onToggleAllLogs: toggleAllLogs,
1150
+ onCopyDetails: copyUpdateDetails,
1151
+ }),
684
1152
  ],
685
1153
  });
686
1154
  }
@@ -34,7 +34,7 @@ export function extractEndpointDetails(source) {
34
34
  const next = markLines.find((l) => l > index);
35
35
  const segment = lines.slice(index, next ?? Math.min(lines.length, index + 400)).join("\n");
36
36
  let methods = "GET/POST";
37
- if (/requireGet\(method,\s*res\)/.test(segment)) methods = "GET";
37
+ if (/requireGet\(method,\s*res\)/.test(segment) || /method\s*!==\s*['"]GET['"]/.test(segment)) methods = "GET";
38
38
  else if (/method\s*!==\s*['"]POST['"]/.test(segment)) methods = "POST";
39
39
  out.set(path, methods);
40
40
  }
@@ -115,7 +115,7 @@ export const REGISTERED_ENDPOINTS = Object.freeze([
115
115
  { path: "/diagnostics", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
116
116
  { path: "/directories", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
117
117
  { path: "/event-detail", methods: "GET", capability: "eventTimeline" },
118
- { path: "/events", methods: "GET/POST", capability: "eventTimeline" },
118
+ { path: "/events", methods: "GET", capability: "eventTimeline" },
119
119
  { path: "/feedback", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
120
120
  { path: "/files", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
121
121
  { path: "/files/upload", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
@@ -136,6 +136,9 @@ export const REGISTERED_ENDPOINTS = Object.freeze([
136
136
  { path: "/notifications", methods: "GET", capability: null, why: NO_CAPABILITY_NEEDED },
137
137
  { path: "/notifications/delete", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
138
138
  { path: "/notifications/read", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
139
+ { path: "/plugin-update/check", methods: "GET", capability: null, auth: "required", why: NO_CAPABILITY_NEEDED },
140
+ { path: "/plugin-update/prepare", methods: "POST", capability: null, auth: "required", why: NO_CAPABILITY_NEEDED },
141
+ { path: "/plugin-update/log", methods: "GET", capability: null, auth: "required", why: NO_CAPABILITY_NEEDED },
139
142
  { path: "/plugin-update", methods: "POST", capability: null, why: NO_CAPABILITY_NEEDED },
140
143
  { path: "/push-test", methods: "GET/POST", capability: null, why: NO_CAPABILITY_NEEDED },
141
144
  { path: "/qr-config", methods: "GET", capability: null, auth: "local-privileged", why: NO_CAPABILITY_NEEDED },