dsh-remote-web 0.7.6 → 0.7.9

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/lib/client.js CHANGED
@@ -1636,6 +1636,33 @@ window.__ModuleLoader__.load({
1636
1636
  try { clearTimeout(t); } catch (e) { /* 忽略 */ }
1637
1637
  }
1638
1638
 
1639
+ // ── 设备超额锁定(契约 docs/device-overquota-lock-contract.md §8)─────────────
1640
+ /**
1641
+ * 「设备被超额锁定」的统一文案(远程访问红条 / 后台服务卡共用,避免两处各写一套漂移)。
1642
+ * 契约 §8.1 要求同时讲清三件事:① 当前套餐可同时用 N 台;② 两条出路(升级 Pro Max / 解绑);
1643
+ * ③ 解绑或升级后**本机会自动解锁**(不是"点重试直到成功",也不能暗示无限重试)。
1644
+ * @param {number|null} limit 服务端给的可用额度;null/未知时用 N 占位
1645
+ */
1646
+ function lockedPanelDetail(limit) {
1647
+ var n = (limit === 0 || limit) ? String(limit) : "N";
1648
+ return "本机已被锁定(当前套餐可同时使用 " + n + " 台)。"
1649
+ + "升级 Pro Max 可多台设备同时使用;或到手机端「管理电脑」解绑其他设备后本机会自动解锁。";
1650
+ }
1651
+ /**
1652
+ * 从云端地址派生「升级 Pro Max」页 URL:`https://host/app/…` → `https://host/app/promo`。
1653
+ * **不硬编码任何域名**(官方/自建/测试环境都走同一个派生规则);拿不到地址返回 "",
1654
+ * 调用方回退到既有「带登录态升级」通道(openUpgradeAuth)。
1655
+ */
1656
+ function upgradePromoUrlOf(base) {
1657
+ var s = String(base || "");
1658
+ if (!s) return "";
1659
+ try {
1660
+ var origin = s.split("/app")[0];
1661
+ if (origin && /^https?:\/\//i.test(origin)) return origin.replace(/\/+$/, "") + "/app/promo";
1662
+ } catch (e) { /* 落到返回空 */ }
1663
+ return "";
1664
+ }
1665
+
1639
1666
  // ── 剪贴板(0.6.9 邀请区「一键复制」)────────────────────────────────────
1640
1667
  /**
1641
1668
  * 复制文本到剪贴板,返回 Promise<boolean>。
@@ -4677,11 +4704,15 @@ window.__ModuleLoader__.load({
4677
4704
  // 状态行优先用连接阶段文案(node 半下发的 user-facing 文案):
4678
4705
  // 「正在准备运行环境(首次约 1~2 分钟)… / 正在启动 Bridge… / 正在连接中继… / 已连接 ✅ …」;
4679
4706
  // 旧版 host 没有 connect 字段 → 完全回退到原来的 running 文案(既有行为不变)。
4707
+ // 锁定态的状态行不能沿用 CONNECT_TEXT.error("连接失败,请按下方提示处理"):用户会以为是自己网络/软件坏了。
4708
+ var connLocked = Boolean(connInfo && connInfo.locked);
4680
4709
  var statusTxt = st === null
4681
4710
  ? "查询中…"
4682
4711
  : !loggedInSaaS
4683
4712
  ? "请先登录(下方账号卡片)后启用远程访问"
4684
- : connInfo ? connText : serviceRunning ? "已连接(可远程访问)" : "等待设备连接";
4713
+ : connInfo
4714
+ ? (connLocked ? "本机已被锁定(当前套餐设备额度已满,升级 Pro Max 或解绑其他设备后自动恢复)" : connText)
4715
+ : serviceRunning ? "已连接(可远程访问)" : "等待设备连接";
4685
4716
  var dotOn = loggedInSaaS ? (connInfo ? connOnline : serviceRunning) : false;
4686
4717
  var dotCls = "dru-dot " + (dotOn ? "dru-dot-on" : "dru-dot-off");
4687
4718
 
@@ -4764,6 +4795,22 @@ window.__ModuleLoader__.load({
4764
4795
  return h("div", null, rows);
4765
4796
  }
4766
4797
 
4798
+ /**
4799
+ * 锁定态的主行动:「🚀 升级 Pro Max」。
4800
+ * 链接从云端账号信息派生(public-config 的 app_url / 面板已拿到的 remoteUrl → origin + /app/promo),
4801
+ * **不硬编码域名**;派生不出地址(未登录/取配置失败)时退回既有「带登录态升级」通道。
4802
+ */
4803
+ function openLockedUpgrade() {
4804
+ var promo = "";
4805
+ try { promo = upgradePromoUrlOf((st && st.remoteUrl) || (pub && pub.app_url)); } catch (e) { promo = ""; }
4806
+ if (promo) {
4807
+ try { window.open(promo, "_blank", "noopener"); } catch (e2) { /* 弹窗被拦截:下面给提示 */ }
4808
+ return;
4809
+ }
4810
+ try { openUpgradeAuth(); } catch (e3) {
4811
+ try { setMessage({ kind: "err", text: "打开升级页失败,请在手机端「我的 → 升级 Pro Max」完成升级" }); } catch (e4) { /* 忽略 */ }
4812
+ }
4813
+ }
4767
4814
  function renderConnectBlock() {
4768
4815
  if (!loggedInSaaS || !connInfo) return null;
4769
4816
  var phase = connInfo.phase || "";
@@ -4773,18 +4820,29 @@ window.__ModuleLoader__.load({
4773
4820
  ? "设备已登记到你的账号:手机端「设备列表」可以看到这台电脑。"
4774
4821
  : "设备已注册到中继:手机端扫码或打开链接即可进入这台电脑。");
4775
4822
  }
4823
+ // 锁定态(契约 §8.1):设备被超额锁定 ≠ 普通故障 —— bridge 活着并在 30s 轮询等解锁,
4824
+ // 所以文案给「升级 / 解绑」两条出路,主行动是升级按钮,且**不显示"自动重试 N 次"**(那会暗示死循环)。
4825
+ var lockedInfo = connInfo.locked && typeof connInfo.locked === "object" ? connInfo.locked : null;
4826
+ var isLocked = Boolean(lockedInfo) || Boolean(connError && connError.code === "device_locked");
4776
4827
  var rows = [];
4777
4828
  if (phase === "error") {
4778
4829
  rows.push(h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 6 } },
4779
- "⚠️ " + ((connError && connError.message) || connText)));
4830
+ (isLocked ? "🔒 " : "⚠️ ") + ((connError && connError.message) || connText)));
4780
4831
  if (connInfo.detail) rows.push(h("div", { className: "dru-hint", style: { marginTop: 4 } }, connInfo.detail));
4781
- if (connInfo.nextRetryInMs > 0) {
4832
+ if (!isLocked && connInfo.nextRetryInMs > 0) {
4782
4833
  rows.push(h("div", { className: "dru-hint", style: { marginTop: 4 } },
4783
4834
  "已自动重试 " + (connInfo.attempts || 0) + " 次,约 " + Math.ceil(connInfo.nextRetryInMs / 1000) + " 秒后自动再试(也可以点下面按钮立刻重试)"));
4784
4835
  }
4785
- rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
4786
- h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: retryConnect }, busy === "connect-retry" ? "重试中…" : "重试"),
4787
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: copyDiagnostics }, copiedDiag ? "已复制" : "复制诊断信息")));
4836
+ if (isLocked) {
4837
+ // 主行动:升级 Pro Max(红色错误条里最该点的就是它)
4838
+ rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
4839
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", onClick: openLockedUpgrade }, "🚀 升级 Pro Max"),
4840
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: copyDiagnostics }, copiedDiag ? "已复制" : "复制诊断信息")));
4841
+ } else {
4842
+ rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
4843
+ h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: retryConnect }, busy === "connect-retry" ? "重试中…" : "重试"),
4844
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: copyDiagnostics }, copiedDiag ? "已复制" : "复制诊断信息")));
4845
+ }
4788
4846
  rows.push(h("div", { className: "dru-hint", style: { marginTop: 6 } },
4789
4847
  "查看日志:后台服务 " + (connInfo.logPath || "") + " ,安装 " + (connInfo.installLogPath || "") + "(把「复制诊断信息」的内容发给客服可加速定位)"));
4790
4848
  var diagToolsErr = renderDiagTools();
@@ -5134,7 +5192,8 @@ window.__ModuleLoader__.load({
5134
5192
  // 自动连接进度(登录后由面板 2.5s 短轮询自动推进):让用户在 bridge 卡也能一眼看到
5135
5193
  // 「到底走到哪一步了」——进程在跑≠能用,注册到中继才算。
5136
5194
  connInfo && connPhase && connPhase !== "no_account"
5137
- ? h("div", { className: "dru-hint", style: { marginTop: 6 } }, "自动连接进度:" + connText)
5195
+ ? h("div", { className: "dru-hint", style: { marginTop: 6 } },
5196
+ "自动连接进度:" + (connInfo.locked ? "本机已被锁定(当前套餐额度已满,升级或解绑后自动恢复)" : connText))
5138
5197
  : null,
5139
5198
  h("div", { className: "dru-actions", style: { marginTop: 10 } },
5140
5199
  !serviceRunning
@@ -5158,13 +5217,22 @@ window.__ModuleLoader__.load({
5158
5217
  "⚠️ 上一次启动失败(自启动入口失效),已自动清理失效自启动项并重新补装运行环境。稍候会自动恢复;仍失败请点「一键更新」。")
5159
5218
  : null,
5160
5219
  st && st.service && st.service.bindError
5161
- ? h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 8 } },
5162
- "⚠️ 设备注册失败:" + (st.service.bindError.message || "未说明原因"),
5163
- h("div", { className: "dru-hint", style: { marginTop: 4 } },
5164
- st.service.bindError.code === "device_limit_exceeded"
5165
- ? "已达本套餐设备数上限。若是同一台电脑重装,稍等片刻会自动顶替旧设备;仍未恢复请在手机端「设备管理」解绑旧设备(免费用户每月可解绑 3 次)后,回到这里点「启动后台服务」。"
5166
- : "请确认网络与账号状态后重试;仍未解决可点下方「彻底卸载」后重新安装。")
5167
- )
5220
+ ? (function (be) {
5221
+ var isLocked = be.code === "device_locked" || be.code === "device_limit_exceeded";
5222
+ var isBindCap = be.code === "device_bind_cap";
5223
+ var beLimit = (be.limit === 0 || be.limit) ? be.limit : null;
5224
+ return h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 8 } },
5225
+ (isLocked ? "🔒 本机已被锁定:" : "⚠️ 设备注册失败:") + (be.message || "未说明原因"),
5226
+ h("div", { className: "dru-hint", style: { marginTop: 4 } },
5227
+ isLocked
5228
+ // 契约 §8.1 的新语义:不是"解绑旧设备才能登录",而是"超额锁定 → 升级或解绑,本机自动解锁"
5229
+ ? lockedPanelDetail(beLimit)
5230
+ : isBindCap
5231
+ // device_bind_cap = 防滥用硬上限(bridge 退出码 5):守护按指数退避重试,不是无限重试
5232
+ ? "已绑定设备数达到上限:请在手机端「管理电脑」解绑不再使用的设备;解绑后本机会自动恢复(后台守护会按指数退避重试,无需反复点按钮)。"
5233
+ : "请确认网络与账号状态后重试;仍未解决可点下方「彻底卸载」后重新安装。")
5234
+ );
5235
+ })(st.service.bindError)
5168
5236
  : null
5169
5237
  ),
5170
5238
  // 版本与更新(自管理:检测新版 / 一键在线更新 / 彻底卸载)
@@ -5638,6 +5706,19 @@ window.__ModuleLoader__.load({
5638
5706
  return "约 " + Math.round(n / 60_000) + " 分钟";
5639
5707
  }
5640
5708
 
5709
+ /**
5710
+ * 宽松读一个「条数」字段:只有真正的数字(JSON number / 纯数字串)才算权威,
5711
+ * null / "" / undefined / true 一律视为**这个字段没有** → 用 fallback。
5712
+ * 为什么要较真:旧 bridge 不认这个键时若把它当 0,面板会**藏起**真实积压(用户以为通知丢了);
5713
+ * 反过来把字段缺失当"有积压"同样不行 —— 那会平白吓人。
5714
+ */
5715
+ function wechatCountOf(value, fallback) {
5716
+ var numeric = typeof value === "number" || (typeof value === "string" && value.trim() !== "");
5717
+ var n = numeric ? Number(value) : NaN;
5718
+ if (!Number.isFinite(n) || n < 0) return fallback;
5719
+ return Math.floor(n);
5720
+ }
5721
+
5641
5722
  /**
5642
5723
  * 🤖 微信机器人绑定面板。
5643
5724
  * @param {{embedded?: boolean}} [props] embedded=true(远程访问面板里的「微信机器人通道」tab)
@@ -6017,6 +6098,16 @@ window.__ModuleLoader__.load({
6017
6098
  // 规格 §9:**只有两态**。这一行是「已绑定 · 健康提示」,两种取值都明确带「已绑定」;
6018
6099
  // 健康只由「最近一次推送是否失败」决定,**不参与绑定状态**(通道没在跑等情形另起一行说明,不冒充第三种状态)。
6019
6100
  var healthy = !wx.last_error;
6101
+ // 🛑 推送静默(2026-10-29):微信只在「你最近发过消息」的很短窗口内允许机器人主动发言,
6102
+ // 窗口外平台没有续期接口(契约 §2.4/§3.4)→ bridge 识别出令牌过期后会**暂停主动推送**
6103
+ // (不再每小时猛打 60 次),并把没送出去的通知留在待补发队列,等你下次说话再补。
6104
+ // 面板这里只做两件事:① 如实说明「为什么暂时收不到推送」;② 给出唯一一条恢复动作(回一句话)。
6105
+ // ⚠️ 它只是**提示**:不参与两态绑定判定,也不隐藏绑定/解绑按钮(规格 §9)。
6106
+ // 字段缺失(旧 bridge 没有这个键)= 没开启静默 → 一律不渲染,绝不能把 undefined 当成"已静默"。
6107
+ var pushPaused = wx.push_paused === true;
6108
+ // 待补发条数:新 bridge 下发 pending_outbox_count(契约 §3.7),旧 bridge 只有 pending_outbox。
6109
+ // 两个字段都在时**只认优先的那一个**,避免同一件事在卡片上提示两遍。
6110
+ var outboxCount = wechatCountOf(wx.pending_outbox_count, wechatCountOf(wx.pending_outbox, 0));
6020
6111
  body.push(h("div", { className: "dru-card" },
6021
6112
  h("h3", null, "已绑定"),
6022
6113
  h("div", { className: "dru-status-line" },
@@ -6030,6 +6121,16 @@ window.__ModuleLoader__.load({
6030
6121
  wechatTimeText(wx.connected_at) ? h("div", { className: "dru-meta" }, "最近上线:" + wechatTimeText(wx.connected_at)) : null,
6031
6122
  Number(wx.pending_replies) > 0 ? h("div", { className: "dru-meta" }, "等待你回复的通知:" + Number(wx.pending_replies) + " 条") : null,
6032
6123
  wx.last_error ? h("div", { className: "dru-msg dru-msg-warn", role: "status", "aria-live": "polite" }, "⚠️ 最近一次推送失败:" + String(wx.last_error)) : null,
6124
+ // 静默提示紧跟 last_error:两者是**两回事**(上面是单次发送失败,这里是"平台侧窗口关了"),
6125
+ // 但都只是健康提示。文案刻意给出可执行的恢复动作,避免用户以为要重新绑定。
6126
+ pushPaused
6127
+ ? h("div", { className: "dru-msg dru-msg-warn", role: "status", "aria-live": "polite" },
6128
+ "⚠️ 微信推送已静默:微信通道只在你最近发过消息后的一小段时间内允许机器人主动发消息。给机器人回任意一条消息即可恢复,未送达的通知会补发。")
6129
+ : null,
6130
+ // 静默期间给一个**简短**的待补发数字(详细说明已由上面的静默提示承担,不再重复一整段)。
6131
+ pushPaused && outboxCount > 0
6132
+ ? h("div", { className: "dru-meta" }, "待补发 " + outboxCount + " 条:你回任意一条消息后会一起补给你。")
6133
+ : null,
6033
6134
  Number(wx.cooldown_ms) > 0
6034
6135
  ? h("div", { className: "dru-msg dru-msg-warn" }, [
6035
6136
  "⏳ 微信侧会话超时,后台已按官方做法退避(还剩 " + wechatCooldownText(wx.cooldown_ms) + ")。",
@@ -6054,9 +6155,11 @@ window.__ModuleLoader__.load({
6054
6155
  ? h("div", { className: "dru-msg dru-msg-warn" },
6055
6156
  "提示:任务事件订阅未就绪(状态 " + String(wx.events_state) + "),这段时间任务跑完可能不会推给你。稍候会自动重连;持续如此请到「📱 远程访问」重启后台服务。")
6056
6157
  : null,
6057
- (Number(wx.pending_outbox) || 0) > 0
6158
+ // 静默态下这条长文案与上面的「静默提示 + 待补发 N 条」重复,故只在**未静默**时显示:
6159
+ // 条数统一走 outboxCount(新 bridge 用 pending_outbox_count,旧 bridge 回落 pending_outbox)。
6160
+ !pushPaused && outboxCount > 0
6058
6161
  ? h("div", { className: "dru-msg dru-msg-warn" },
6059
- "有 " + Number(wx.pending_outbox) + " 条通知刚才没能发出去(微信侧发送失败),已排在待补发队列里 —— 你下次在微信里任意发一句话,就会自动补给你。")
6162
+ "有 " + outboxCount + " 条通知刚才没能发出去(微信侧发送失败),已排在待补发队列里 —— 你下次在微信里任意发一句话,就会自动补给你。")
6060
6163
  : null,
6061
6164
  wx.last_fault && wx.last_fault.code
6062
6165
  ? h("div", { className: "dru-hint" },