@mrrisega/dsh-remote 0.6.8 → 0.6.9-beta.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.
@@ -46,36 +46,46 @@ window.__ModuleLoader__.load({
46
46
  ".dru-settings-head{display:flex;align-items:center;gap:10px;padding:6px 2px 2px}",
47
47
  ".dru-settings-icon{font-size:24px;line-height:1;flex:none}",
48
48
  ".dru-settings-title{margin:0;font-size:17px;font-weight:600;color:var(--dsw-alias-label-primary,#e6edf3)}",
49
- ".dru-settings-sub{font-size:12px;color:var(--dsw-alias-label-tertiary,#8c959f);margin-top:2px}",
49
+ // 副标题用 #57606a(对 #f6f8fa 卡片底色 6.0:1,达 AA 正文标准);旧的 #8c959f 只有 2.85:1
50
+ ".dru-settings-sub{font-size:12.5px;color:var(--dsw-alias-label-tertiary,#57606a);margin-top:3px;line-height:1.6}",
50
51
  ".dru-settings-body{display:flex;flex-direction:column;gap:14px}",
51
52
  // 首次安装引导小红点(挂在设置页「远程访问」导航栏目右上角)
52
53
  ".dru-reddot{position:absolute;top:9px;right:12px;width:7px;height:7px;border-radius:50%;background:#e5484d;box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2,#fff);pointer-events:none;z-index:1}",
53
54
  ".dru-card{background:#f6f8fa;border:1px solid #eaeef2;border-radius:10px;padding:14px 16px}",
54
55
  ".dru-card h3{margin:0 0 8px;font-size:13px;font-weight:700;color:#1f2328}",
55
56
  ".dru-url{background:#ffffff;border:1px solid #d0d7de;border-radius:8px;padding:9px 11px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;word-break:break-all;display:flex;align-items:center;justify-content:space-between;gap:8px;color:#1f2328}",
56
- ".dru-url button{flex:none;border:1px solid #d0d7de;background:#ffffff;color:#0969da;border-radius:6px;padding:3px 8px;font-size:12px;cursor:pointer}",
57
- ".dru-url button:hover{background:#f6f8fa}",
57
+ ".dru-url button{flex:none;min-height:44px;padding:10px 14px;border:1px solid #d0d7de;background:#ffffff;color:#0969da;border-radius:6px;font-size:12.5px;font-weight:600;cursor:pointer;font-family:inherit;transition:background .12s ease,border-color .12s ease}",
58
+ ".dru-url button:hover{background:#f6f8fa;border-color:#0969da}",
59
+ ".dru-url button:active{background:#eaeef2}",
58
60
  ".dru-field{margin-bottom:11px}",
59
61
  ".dru-field > label{display:block;font-size:12.5px;font-weight:600;color:#1f2328;margin-bottom:5px}",
60
- ".dru-input{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13.5px;font-family:inherit}",
61
- ".dru-input:focus{outline:none;border-color:#0969da;box-shadow:0 0 0 3px rgba(9,105,218,.15)}",
62
+ ".dru-input{width:100%;box-sizing:border-box;min-height:44px;padding:10px 11px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13.5px;font-family:inherit}",
63
+ ".dru-input:focus{border-color:#0969da;box-shadow:0 0 0 3px rgba(9,105,218,.28)}",
62
64
  ".dru-actions{display:flex;gap:8px;flex-wrap:wrap}",
63
- ".dru-btn{padding:8px 14px;border-radius:8px;border:1px solid transparent;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit}",
65
+ // 触控目标 ≥44px(相邻 8px,见 .dru-actions/.dru-tabs gap):按钮统一 min-height + hover/active 反馈
66
+ ".dru-btn{min-height:44px;padding:10px 15px;border-radius:8px;border:1px solid transparent;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;transition:background .12s ease,border-color .12s ease,box-shadow .12s ease}",
64
67
  ".dru-btn:disabled{opacity:.55;cursor:default}",
65
68
  ".dru-btn-primary{background:#0969da;color:#ffffff;border-color:#0969da}",
66
69
  ".dru-btn-primary:hover:not(:disabled){background:#0860bd}",
70
+ ".dru-btn-primary:active:not(:disabled){background:#0757a8;border-color:#0757a8}",
67
71
  ".dru-btn-ghost{background:#ffffff;color:#1f2328;border-color:#d0d7de}",
68
- ".dru-btn-ghost:hover:not(:disabled){background:#f6f8fa}",
72
+ ".dru-btn-ghost:hover:not(:disabled){background:#f6f8fa;border-color:#0969da}",
73
+ ".dru-btn-ghost:active:not(:disabled){background:#eaeef2}",
69
74
  ".dru-btn-danger{background:#ffffff;color:#cf222e;border-color:#cf222e}",
70
75
  ".dru-btn-danger:hover:not(:disabled){background:#fff0f1}",
71
- ".dru-linkbtn{display:inline-block;padding:0;border:none;background:none;color:#0969da;font-size:12.5px;line-height:1.6;cursor:pointer;font-family:inherit;text-decoration:none}",
76
+ ".dru-btn-danger:active:not(:disabled){background:#ffdfe0}",
77
+ // 行内文字按钮:视觉高度不变(不破坏排版),用 ::after 把命中区扩到 ≥44px
78
+ ".dru-linkbtn{display:inline-block;position:relative;padding:0;border:none;background:none;color:#0969da;font-size:12.5px;line-height:1.7;cursor:pointer;font-family:inherit;text-decoration:none}",
79
+ ".dru-linkbtn::after{content:\"\";position:absolute;left:-8px;right:-8px;top:-12px;bottom:-12px}",
72
80
  ".dru-linkbtn:hover{text-decoration:underline}",
81
+ ".dru-linkbtn:active{color:#0550ae}",
73
82
  ".dru-tabs{display:flex;gap:8px;margin-bottom:12px}",
74
- ".dru-tab{flex:1;padding:7px 0;text-align:center;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;color:#57606a;background:#f6f8fa;border:1px solid #d0d7de;user-select:none}",
83
+ ".dru-tab{flex:1;min-height:44px;display:flex;align-items:center;justify-content:center;padding:8px 0;text-align:center;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;color:#57606a;background:#f6f8fa;border:1px solid #d0d7de;user-select:none;transition:background .12s ease,border-color .12s ease}",
84
+ ".dru-tab:hover{background:#ffffff;border-color:#0969da;color:#0969da}",
75
85
  ".dru-tab.active{color:#0969da;background:#ffffff;border-color:#0969da}",
76
86
  ".dru-captcha{display:flex;align-items:stretch;gap:8px}",
77
87
  ".dru-captcha .dru-input{flex:1;min-width:0}",
78
- ".dru-captcha-box{width:118px;height:42px;flex:none;border-radius:8px;border:1px solid #d0d7de;cursor:pointer;background:#f6f8fa;display:flex;align-items:center;justify-content:center;font-size:12px;color:#57606a;overflow:hidden}",
88
+ ".dru-captcha-box{width:118px;height:44px;flex:none;border-radius:8px;border:1px solid #d0d7de;cursor:pointer;background:#f6f8fa;display:flex;align-items:center;justify-content:center;font-size:12px;color:#57606a;overflow:hidden}",
79
89
  ".dru-captcha-box svg{display:block;width:100%;height:100%}",
80
90
  ".dru-user{display:flex;align-items:center;gap:10px;margin-bottom:12px}",
81
91
  ".dru-avatar{width:40px;height:40px;border-radius:50%;background:#0969da;color:#ffffff;display:flex;align-items:center;justify-content:center;font-size:17px;font-weight:700;flex-shrink:0}",
@@ -84,7 +94,7 @@ window.__ModuleLoader__.load({
84
94
  ".dru-status-line{display:flex;align-items:center;gap:8px;font-size:13px;color:#1f2328}",
85
95
  ".dru-dot{width:8px;height:8px;border-radius:50%;display:inline-block;flex:none}",
86
96
  ".dru-dot-on{background:#1a7f37;box-shadow:0 0 6px rgba(26,127,55,.6)}",
87
- ".dru-dot-off{background:#8c959f}",
97
+ ".dru-dot-off{background:#6e7781}",
88
98
  ".dru-meta{font-size:12px;color:#57606a;margin-top:6px;word-break:break-all}",
89
99
  ".dru-msg{font-size:12.5px;min-height:18px;margin-top:8px}",
90
100
  ".dru-msg-ok{color:#1a7f37}",
@@ -100,11 +110,12 @@ window.__ModuleLoader__.load({
100
110
  ".dru-restart-foot .dru-hint{flex:1;min-width:180px;margin:0}",
101
111
  // ── 用户反馈模块 ──
102
112
  ".dru-fb-tabs{display:flex;gap:8px;margin-bottom:10px}",
103
- ".dru-fb-tab{flex:1;padding:6px 0;text-align:center;border-radius:8px;cursor:pointer;font-size:12.5px;font-weight:600;color:#57606a;background:#eaeef2;border:1px solid #d0d7de;user-select:none}",
113
+ ".dru-fb-tab{flex:1;min-height:44px;display:flex;align-items:center;justify-content:center;padding:8px 0;text-align:center;border-radius:8px;cursor:pointer;font-size:12.5px;font-weight:600;color:#57606a;background:#eaeef2;border:1px solid #d0d7de;user-select:none}",
104
114
  ".dru-fb-tab.active{color:#0969da;background:#ffffff;border-color:#0969da}",
105
115
  ".dru-fb-select{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13.5px;font-family:inherit}",
106
116
  ".dru-fb-textarea{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13.5px;font-family:inherit;resize:vertical;min-height:64px}",
107
- ".dru-fb-textarea:focus{outline:none;border-color:#0969da;box-shadow:0 0 0 3px rgba(9,105,218,.15)}",
117
+ ".dru-fb-textarea:focus{border-color:#0969da;box-shadow:0 0 0 3px rgba(9,105,218,.28)}",
118
+ ".dru-fb-select:focus{border-color:#0969da;box-shadow:0 0 0 3px rgba(9,105,218,.28)}",
108
119
  ".dru-fb-item{border:1px solid #eaeef2;border-radius:8px;background:#ffffff;padding:10px 12px;margin-bottom:8px}",
109
120
  ".dru-fb-item-head{display:flex;align-items:center;gap:8px;margin-bottom:4px;flex-wrap:wrap}",
110
121
  ".dru-fb-badge{font-size:11px;border-radius:999px;padding:1px 8px;flex:none}",
@@ -114,14 +125,14 @@ window.__ModuleLoader__.load({
114
125
  ".dru-fb-cat{font-size:11px;border-radius:999px;padding:1px 8px;flex:none;color:#57606a;background:#f6f8fa;border:1px solid #d0d7de}",
115
126
  ".dru-fb-item-title{font-size:13px;font-weight:600;color:#1f2328;flex:1;min-width:120px}",
116
127
  ".dru-fb-item-content{font-size:12.5px;color:#57606a;white-space:pre-wrap;word-break:break-word;margin:4px 0}",
117
- ".dru-fb-item-time{font-size:11px;color:#8c959f}",
128
+ ".dru-fb-item-time{font-size:11.5px;color:#57606a}",
118
129
  ".dru-fb-reply{border-top:1px dashed #eaeef2;margin-top:8px;padding-top:8px}",
119
130
  ".dru-fb-reply-row{display:flex;gap:6px;align-items:flex-start;margin-bottom:6px}",
120
131
  ".dru-fb-reply-who{font-size:12px;font-weight:600;color:#0969da;flex:none;width:76px}",
121
132
  ".dru-fb-reply-who.user{color:#57606a}",
122
133
  ".dru-fb-reply-text{font-size:12.5px;color:#1f2328;white-space:pre-wrap;word-break:break-word;flex:1}",
123
134
  ".dru-fb-reply-input{width:100%;box-sizing:border-box;padding:7px 10px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;font-size:12.5px;font-family:inherit;resize:vertical;min-height:44px}",
124
- ".dru-fb-empty{font-size:12.5px;color:#8c959f;text-align:center;padding:14px 0}",
135
+ ".dru-fb-empty{font-size:12.5px;color:#57606a;text-align:center;padding:14px 0}",
125
136
  // ── 满意度弹窗(1 小时体验后,只弹一次) ──
126
137
  ".dru-popup{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:2147482000;display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}",
127
138
  ".dru-popup-card{width:min(400px,calc(100vw - 48px));background:#ffffff;color:#1f2328;border:1px solid #d0d7de;border-radius:12px;box-shadow:0 24px 64px rgba(0,0,0,.45);font-size:14px;line-height:1.5;font-family:var(--dsw-font-family,-apple-system,'PingFang SC','Microsoft YaHei',sans-serif);overflow:hidden}",
@@ -137,9 +148,9 @@ window.__ModuleLoader__.load({
137
148
  ".dru-popup-textarea{width:100%;box-sizing:border-box;padding:9px 11px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13px;font-family:inherit;resize:vertical;min-height:56px;text-align:left}",
138
149
  ".dru-popup-invite{background:#f6f8fa;border:1px dashed #d0d7de;border-radius:8px;padding:12px;font-size:13px;color:#57606a;text-align:center;margin-bottom:12px}",
139
150
  ".dru-popup-actions{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-top:14px}",
140
- ".dru-popup-foot{display:flex;justify-content:space-between;padding:10px 22px;border-top:1px solid #eaeef2;font-size:12px;color:#8c959f}",
141
- ".dru-popup-foot button{border:none;background:none;color:#57606a;cursor:pointer;font-size:12px;font-family:inherit;padding:4px 6px}",
142
- ".dru-popup-foot button:hover{color:#0969da}",
151
+ ".dru-popup-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 16px;border-top:1px solid #eaeef2;font-size:12px;color:#57606a}",
152
+ ".dru-popup-foot button{min-height:44px;border:none;background:none;color:#57606a;cursor:pointer;font-size:12.5px;font-family:inherit;padding:10px 8px;border-radius:6px}",
153
+ ".dru-popup-foot button:hover{color:#0969da;background:#f6f8fa}",
143
154
  ".dru-popup .dru-msg{text-align:left}",
144
155
  ".dru-community-qr{display:block;width:220px;max-width:62vw;margin:0 auto;background:#ffffff;padding:10px;border-radius:10px;border:1px solid #d0d7de;box-sizing:content-box}",
145
156
  ".dru-restart-auto{display:flex;align-items:center;gap:8px;margin-top:10px;font-size:12.5px;color:#1a7f37;background:#dafbe1;border-radius:8px;padding:8px 10px}",
@@ -151,11 +162,63 @@ window.__ModuleLoader__.load({
151
162
  ".dru-ver-badge-new{color:#9a6700;background:#fff8c5;border:1px solid #eed888}",
152
163
  ".dru-ver-badge-ok{color:#1a7f37;background:#dafbe1;border:1px solid #aceebb}",
153
164
  ".dru-up-log{margin-top:8px;background:#0d1117;color:#e6edf3;border-radius:8px;padding:8px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:150px;overflow:auto}",
165
+ // 版本卡信息层级(0.6.9):版本号大字 + 通道 chip + 指标行(已用时/无输出)+ 卡住告警
166
+ ".dru-ver-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
167
+ ".dru-ver-num{font-size:15px;font-weight:700;color:#1f2328}",
168
+ ".dru-ver-chip{font-size:11.5px;border-radius:999px;padding:2px 9px;color:#0a3069;background:#ddf4ff;border:1px solid #b6e3ff;white-space:nowrap}",
169
+ ".dru-ver-metrics{margin-top:9px;background:#ffffff;border:1px solid #d0d7de;border-radius:8px;padding:9px 11px}",
170
+ ".dru-ver-metric{display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12.5px;line-height:1.9;color:#1f2328}",
171
+ ".dru-ver-metric > span:first-child{color:#57606a}",
172
+ ".dru-stall{margin-top:9px;background:#fff8c5;border:1px solid #d4a72c;border-radius:8px;padding:10px 12px;font-size:12.5px;line-height:1.65;color:#6b5900}",
173
+ ".dru-stall b{color:#7d4e00}",
174
+ ".dru-spin{width:13px;height:13px;border-radius:50%;border:2px solid #d0d7de;border-top-color:#0969da;display:inline-block;vertical-align:-2px;animation:dru-spin .9s linear infinite}",
175
+ "@keyframes dru-spin{to{transform:rotate(360deg)}}",
176
+ // ── 🎯 邀请好友(0.6.9):奖励前置(大字)→ 进度 → 邀请码/链接(一键复制)→ 折叠规则 → 记录 ──
177
+ ".dru-invite-hero{font-size:20px;line-height:1.4;font-weight:700;color:#1f2328;margin:2px 0 8px}",
178
+ ".dru-invite-hero em{font-style:normal;color:#0550ae}",
179
+ ".dru-invite-sub{font-size:13px;line-height:1.65;color:#57606a}",
180
+ ".dru-invite-prog{margin-top:12px;background:#ffffff;border:1px solid #d0d7de;border-radius:8px;padding:10px 12px}",
181
+ ".dru-invite-prog-top{display:flex;align-items:baseline;justify-content:space-between;gap:8px;flex-wrap:wrap;font-size:13px;font-weight:600;color:#1f2328}",
182
+ ".dru-invite-prog-top .n{font-size:15px;font-weight:700;color:#0550ae}",
183
+ ".dru-invite-bar{height:8px;border-radius:999px;background:#eaeef2;overflow:hidden;margin:9px 0 7px}",
184
+ ".dru-invite-bar-fill{height:100%;border-radius:999px;background:#0969da;transition:width .3s ease}",
185
+ ".dru-invite-next{font-size:12.5px;line-height:1.65;color:#57606a}",
186
+ ".dru-copy-row{margin-top:12px}",
187
+ ".dru-copy-row:first-child{margin-top:4px}",
188
+ ".dru-copy-label{font-size:12.5px;font-weight:600;color:#1f2328;margin-bottom:6px}",
189
+ ".dru-copy-box{display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:#ffffff;border:1px solid #d0d7de;border-radius:8px;padding:9px 11px}",
190
+ ".dru-code-val{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:19px;font-weight:700;letter-spacing:2.5px;color:#1f2328;flex:1;min-width:140px;word-break:break-all}",
191
+ ".dru-link-val{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;line-height:1.55;color:#1f2328;flex:1;min-width:180px;word-break:break-all;overflow-wrap:anywhere}",
192
+ ".dru-copy-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;flex:none;min-height:44px;padding:10px 16px;border-radius:8px;border:1px solid #0969da;background:#0969da;color:#ffffff;font:600 13px/1.2 inherit;cursor:pointer;transition:background .12s ease,border-color .12s ease}",
193
+ ".dru-copy-btn:hover:not(:disabled){background:#0860bd;border-color:#0860bd}",
194
+ ".dru-copy-btn:active:not(:disabled){background:#0757a8;border-color:#0757a8}",
195
+ ".dru-copy-btn:disabled{opacity:.55;cursor:default}",
196
+ ".dru-copy-btn.copied{background:#1a7f37;border-color:#1a7f37}",
197
+ ".dru-empty{text-align:center;padding:20px 14px;background:#ffffff;border:1px dashed #d0d7de;border-radius:10px}",
198
+ ".dru-empty-icon{font-size:26px;line-height:1;margin-bottom:8px}",
199
+ ".dru-empty-title{font-size:13.5px;font-weight:600;color:#1f2328;margin-bottom:5px}",
200
+ ".dru-empty-sub{font-size:12.5px;line-height:1.7;color:#57606a;max-width:380px;margin:0 auto}",
201
+ ".dru-empty-actions{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:14px}",
202
+ ".dru-disclose{width:100%;box-sizing:border-box;min-height:44px;display:flex;align-items:center;justify-content:space-between;gap:10px;background:#ffffff;border:1px solid #d0d7de;border-radius:8px;padding:10px 12px;font:600 13px/1.3 inherit;color:#1f2328;cursor:pointer;text-align:left}",
203
+ ".dru-disclose:hover{background:#f6f8fa;border-color:#0969da}",
204
+ ".dru-disclose-caret{font-size:12px;font-weight:600;color:#57606a;flex:none}",
205
+ ".dru-rules{margin:10px 0 0;padding-left:20px;font-size:12.5px;line-height:1.8;color:#57606a}",
206
+ ".dru-rules li{margin-bottom:6px}",
207
+ ".dru-rules b{color:#1f2328}",
208
+ ".dru-rec{background:#ffffff;border:1px solid #eaeef2;border-radius:8px;padding:10px 12px;margin-bottom:8px}",
209
+ ".dru-rec-top{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
210
+ ".dru-rec-who{font-size:13px;font-weight:600;color:#1f2328;flex:1;min-width:120px;word-break:break-all}",
211
+ ".dru-rec-when{font-size:12px;color:#57606a}",
212
+ ".dru-rec-tag{font-size:11.5px;border-radius:999px;padding:1px 9px;flex:none;white-space:nowrap}",
213
+ ".dru-rec-tag-ok{color:#116329;background:#dafbe1;border:1px solid #aceebb}",
214
+ ".dru-rec-tag-wait{color:#9a6700;background:#fff8c5;border:1px solid #eed888}",
215
+ ".dru-rec-sub{font-size:12px;color:#57606a;margin-top:5px;line-height:1.6}",
216
+ ".dru-rec-head{display:flex;gap:8px;font-size:11.5px;font-weight:600;color:#57606a;padding:0 4px 7px;border-bottom:1px solid #eaeef2;margin-bottom:9px}",
154
217
  // ── 📱 远程访问(一次性访问密钥 + 已授权设备管理) ──
155
218
  ".dru-access-flex{display:flex;gap:14px;flex-wrap:wrap;align-items:flex-start;margin-top:10px}",
156
219
  ".dru-access-col{flex:1;min-width:230px;display:flex;flex-direction:column;gap:8px}",
157
220
  ".dru-qr-img{width:180px;height:180px;flex:none;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;object-fit:contain}",
158
- ".dru-qr-ph{width:180px;height:180px;flex:none;border-radius:8px;border:1px dashed #d0d7de;background:#f6f8fa;color:#8c959f;font-size:12px;display:flex;align-items:center;justify-content:center;text-align:center;padding:10px;box-sizing:border-box}",
221
+ ".dru-qr-ph{width:180px;height:180px;flex:none;border-radius:8px;border:1px dashed #d0d7de;background:#f6f8fa;color:#57606a;font-size:12px;display:flex;align-items:center;justify-content:center;text-align:center;padding:10px;box-sizing:border-box}",
159
222
  ".dru-url.big{font-size:13.5px;font-weight:600}",
160
223
  ".dru-cd{font-size:12px;color:#9a6700;margin-top:2px}",
161
224
  ".dru-cd-ok{color:#1a7f37}",
@@ -164,8 +227,8 @@ window.__ModuleLoader__.load({
164
227
  ".dru-dev-top{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
165
228
  ".dru-dev-name{font-size:13px;font-weight:600;color:#1f2328;flex:1;min-width:130px}",
166
229
  ".dru-dev-meta{font-size:11.5px;color:#57606a}",
167
- ".dru-dev-sub{font-size:11px;color:#8c959f;margin-top:3px}",
168
- ".dru-dev-tag{font-size:10.5px;color:#8c959f;border:1px solid #d0d7de;border-radius:999px;padding:0 7px;flex:none;white-space:nowrap}",
230
+ ".dru-dev-sub{font-size:11.5px;color:#57606a;margin-top:4px}",
231
+ ".dru-dev-tag{font-size:11.5px;color:#57606a;border:1px solid #d0d7de;border-radius:999px;padding:0 7px;flex:none;white-space:nowrap}",
169
232
  ".dru-dev-tag-off{color:#cf222e;border-color:#ffb3b6;background:#fff0f1}",
170
233
  // Phase-5:端到端加密(E2EE)状态行 —— 启用=绿字绿点,未启用=灰字(纯文字状态行,不打扰)
171
234
  ".dru-e2ee-line{display:flex;align-items:center;gap:7px;margin-top:6px;font-size:12px;line-height:1.5;color:#57606a}",
@@ -175,6 +238,25 @@ window.__ModuleLoader__.load({
175
238
  ".dru-nav-remote{display:inline-flex;align-items:center;gap:6px;height:30px;margin:0 0 2px;padding:0 10px;border:1px solid #d0d7de;border-radius:8px;background:#ffffff;color:#0969da;font:500 12.5px/1 inherit;cursor:pointer;white-space:nowrap;user-select:none}",
176
239
  ".dru-nav-remote:hover:not(:disabled){background:#f0f6ff;border-color:#0969da}",
177
240
  ".dru-nav-remote-dot{position:absolute;top:-2px;right:-2px;width:8px;height:8px;border-radius:50%;background:#e5484d;box-shadow:0 0 0 2px #fff;pointer-events:none}",
241
+ // 侧栏入口与官方按钮同高(30px);用 ::after 把命中区补到 ≥44px,不破坏与原按钮的对齐
242
+ ".dru-nav-remote::after{content:\"\";position:absolute;left:0;right:0;top:-7px;bottom:-7px}",
243
+ // ── 无障碍基线(0.6.9)────────────────────────────────────────────────
244
+ // 键盘焦点必须可见:放在样式表末尾,覆盖上面任何 outline:none(同特异性后者胜)
245
+ ".dru-settings-section :focus-visible,.dru-popup :focus-visible{outline:2px solid #0969da;outline-offset:2px}",
246
+ ".dru-settings-section [role=\"region\"]:focus-visible{outline-offset:-2px}",
247
+ ".dru-btn:focus-visible,.dru-copy-btn:focus-visible,.dru-tab:focus-visible,.dru-disclose:focus-visible{outline:2px solid #0969da;outline-offset:2px;box-shadow:0 0 0 4px rgba(9,105,218,.18)}",
248
+ // 尊重系统「减少动态效果」:关掉过渡与旋转动画(状态文字照常更新)
249
+ "@media (prefers-reduced-motion: reduce){",
250
+ " .dru-btn,.dru-copy-btn,.dru-tab,.dru-disclose,.dru-invite-bar-fill,.dru-nav-remote,.dru-input,.dru-fb-tab,.dru-popup-rate button{transition:none !important}",
251
+ " .dru-spin{animation:none !important;border-top-color:#0969da}",
252
+ "}",
253
+ // 窄屏(手机竖屏打开本机面板):卡片内边距收紧、代码/链接不溢出
254
+ "@media (max-width:520px){",
255
+ " .dru-card{padding:12px 12px}",
256
+ " .dru-invite-hero{font-size:18px}",
257
+ " .dru-copy-box{flex-direction:column;align-items:stretch}",
258
+ " .dru-copy-btn{width:100%}",
259
+ "}",
178
260
  ].join("\n");
179
261
  document.head.appendChild(styleEl);
180
262
 
@@ -434,6 +516,62 @@ window.__ModuleLoader__.load({
434
516
  try { clearTimeout(t); } catch (e) { /* 忽略 */ }
435
517
  }
436
518
 
519
+ // ── 剪贴板(0.6.9 邀请区「一键复制」)────────────────────────────────────
520
+ /**
521
+ * 复制文本到剪贴板,返回 Promise<boolean>。
522
+ * 优先 Clipboard API;http 非安全上下文 / 旧浏览器里 navigator.clipboard 不存在
523
+ * → 退回隐藏 textarea + document.execCommand("copy");两种都失败返回 false,
524
+ * 调用方**必须**在按钮旁给出可见提示(不能让用户以为"点了没反应")。
525
+ */
526
+ function copyText(text) {
527
+ var s = text == null ? "" : String(text);
528
+ if (!s) return Promise.resolve(false);
529
+ var fallback = function () { return Promise.resolve(copyTextLegacy(s)); };
530
+ try {
531
+ if (navigator && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
532
+ return navigator.clipboard.writeText(s).then(function () { return true; }).catch(fallback);
533
+ }
534
+ } catch (e) { /* 落入兜底 */ }
535
+ return fallback();
536
+ }
537
+ /** 兜底复制:隐藏 textarea + execCommand(同步;不可用/被拒绝时返回 false)。 */
538
+ function copyTextLegacy(s) {
539
+ var ta = null;
540
+ try {
541
+ ta = document.createElement("textarea");
542
+ ta.value = s;
543
+ ta.setAttribute("readonly", "readonly");
544
+ ta.style.position = "fixed";
545
+ ta.style.top = "-1000px";
546
+ ta.style.left = "-1000px";
547
+ ta.style.opacity = "0";
548
+ document.body.appendChild(ta);
549
+ try { ta.select(); } catch (e) { /* 某些环境无 select */ }
550
+ return !!(document.execCommand && document.execCommand("copy"));
551
+ } catch (e) {
552
+ return false;
553
+ } finally {
554
+ try { if (ta && ta.parentNode && ta.parentNode.removeChild) ta.parentNode.removeChild(ta); } catch (e) { /* 忽略 */ }
555
+ }
556
+ }
557
+ /** 毫秒 → 人话时长("12 秒" / "1 分 05 秒" / "1 小时 02 分");用于更新「已用时」。 */
558
+ function fmtDuration(ms) {
559
+ var n = Number(ms);
560
+ var s = Math.max(0, Math.round((isFinite(n) ? n : 0) / 1000));
561
+ if (s < 60) return s + " 秒";
562
+ var m = Math.floor(s / 60);
563
+ if (m < 60) return m + " 分 " + (s % 60 < 10 ? "0" : "") + (s % 60) + " 秒";
564
+ return Math.floor(m / 60) + " 小时 " + (m % 60 < 10 ? "0" : "") + (m % 60) + " 分";
565
+ }
566
+ /** 手机号掩码(邀请记录里的好友手机号:面板只展示掩码,完整号码留在服务端)。 */
567
+ function maskPhoneLike(v) {
568
+ var s = String(v == null ? "" : v).trim();
569
+ if (!s) return "";
570
+ if (/^\d{11}$/.test(s)) return s.slice(0, 3) + "****" + s.slice(7);
571
+ if (/^\d{7,}$/.test(s)) return s.slice(0, 3) + "****" + s.slice(-2);
572
+ return s;
573
+ }
574
+
437
575
  /** 一次性访问密钥 / 已授权设备列表的自动重试状态(放在组件外:不随渲染重建)。 */
438
576
  var akeyRetry = { timer: null, attempt: 0 };
439
577
  var devRetry = { timer: null, attempt: 0 };
@@ -453,6 +591,13 @@ window.__ModuleLoader__.load({
453
591
  */
454
592
  var connFailRef = { v: 0 };
455
593
  var CONN_FAIL_VISIBLE = 3; // ≈ 2.5s × 3:既要够快让用户看见,又要避免一次抖动就报红
594
+ /**
595
+ * 「连接偏慢」的本地观察起点(0 = 不慢)——用于在用户干等时给一个轻量邀请入口。
596
+ * 只用既有 /dsh-remote/bridge-status 下发字段 + 本地计时,**不新增也不假设后端字段**;
597
+ * 观察不到就永远不显示(宁可不出现,也不误报)。
598
+ */
599
+ var slowConnTrack = { since: 0 };
600
+ var SLOW_CONNECT_HINT_MS = 90000; // 非 online 阶段持续 90s 才认为「明显偏慢」
456
601
  /** 连接阶段轮询节奏:未连通时 2.5s(自动推进),已连接后退避到 15s;页面隐藏时完全停下。 */
457
602
  var CONN_POLL_FAST_MS = 2500;
458
603
  var CONN_POLL_SLOW_MS = 15000;
@@ -956,15 +1101,18 @@ window.__ModuleLoader__.load({
956
1101
  h("div", { className: "dru-url", style: { marginTop: 10, textAlign: "left" } },
957
1102
  h("span", null, popupInvite.link),
958
1103
  h("button", { type: "button", onClick: function () {
959
- try { navigator.clipboard.writeText(popupInvite.link).then(function () { setInviteCopied(true); setTimeout(function () { setInviteCopied(false); }, 1500); }); } catch (e) {}
1104
+ copyText(popupInvite.link).then(function (done) {
1105
+ setInviteCopied(done);
1106
+ if (done) later(function () { setInviteCopied(false); }, 2000);
1107
+ });
960
1108
  } }, inviteCopied ? "已复制" : "复制邀请链接")
961
1109
  ),
962
1110
  h("div", { style: { marginTop: 8, fontSize: 12 } }, "把链接发给好友,注册时自动带上你的邀请码。")
963
1111
  )
964
1112
  : h("div", { style: { marginTop: 6, fontSize: 12 } },
965
1113
  popupInvite && popupInvite.code
966
- ? "邀请码 " + popupInvite.code + " 已生成:在设置面板 →「🎯 邀请好友赚会员」中复制邀请链接。"
967
- : "登录手机号账号后,在设置面板 →「🎯 邀请好友赚会员」中获取专属邀请链接。")
1114
+ ? "邀请码 " + popupInvite.code + " 已生成:在设置面板 →「🎯 邀请好友,一起用远程访问」里复制邀请链接。"
1115
+ : "登录手机号账号后,在设置面板 →「🎯 邀请好友,一起用远程访问」里获取专属邀请链接。")
968
1116
  )
969
1117
  : h("div", null,
970
1118
  h("div", { className: "dru-popup-actions" },
@@ -992,10 +1140,50 @@ window.__ModuleLoader__.load({
992
1140
  // (原 🖥 已随入口迁移调整);栏目导航 label 与栏目头部均使用它。
993
1141
 
994
1142
  // ── 版本与更新卡片(自管理:市场没有更新按钮,这里提供在线一键更新/彻底卸载) ──
995
- // 数据来自 node 半新增的 /dsh-remote/self* 路由;逻辑均在插件 node 半实现,
1143
+ // 数据来自 node 半的 /dsh-remote/self* 路由;逻辑均在插件 node 半实现,
996
1144
  // 因此无论插件从「插件市场」还是 npx 安装,界面与行为完全一致。
1145
+ //
1146
+ // 【0.6.9 冻结契约】——**每个新字段都必须特性检测**,缺字段要优雅降级
1147
+ // (否则面板会在旧 node 半上白屏/报错):
1148
+ // GET /dsh-remote/self → { version, runtimeReady, channel? }
1149
+ // GET /dsh-remote/self/update-log → { running, log, failure?, channel?,
1150
+ // startedAt?, elapsedMs?, idleMs?, idleThresholdMs?, stalled? }
1151
+ // POST /dsh-remote/self/update → { ok, channel? }
1152
+ // POST /dsh-remote/self/update/cancel → { ok, killed, cleared }(旧 node 半无此路由 → 404 → 隐藏按钮并给一句人话)
1153
+ // failure.failCode 可能是 update_stalled(看门狗判定无输出)/ user_cancelled(用户主动取消)等
1154
+ // 「已用时 / 无输出时长」在字段缺失时用本地观测兜底(startedAt/日志变化时间),保证旧宿主也看得见。
1155
+ var UPDATE_IDLE_HINT_MS = 20000; // 无新日志超过 20s → 灰字提示(不惊动用户)
1156
+ var UPDATE_CANCEL_IDLE_MS = 90000; // 超过 90s 无变化 → 出现「取消并重试」(旧宿主无 idleThresholdMs 时的兜底)
1157
+ /** 更新通道 → 人话(channel 也可能是具体版本号)。 */
1158
+ var UPDATE_CHANNEL_COPY = { latest: "稳定版", beta: "预览版", next: "预览版", rc: "候选版" };
1159
+ function updateChannelText(ch) {
1160
+ var c = String(ch || "").trim();
1161
+ if (!c) return "";
1162
+ var friendly = UPDATE_CHANNEL_COPY[c];
1163
+ if (friendly) return friendly + "(" + c + ")";
1164
+ if (/^\d/.test(c)) return "指定版本(" + c + ")";
1165
+ return c;
1166
+ }
1167
+ /**
1168
+ * fail_code → 人话补一句。**只映射能确定的码**;未知码返回空串(不编造原因,
1169
+ * 由服务端 detail 说明)。update_stalled 是 0.6.9 看门狗新增:更新进程长时间无输出。
1170
+ */
1171
+ var UPDATE_FAIL_COPY = {
1172
+ update_stalled: "更新进程长时间没有响应,已自动换源重试;若长时间没有变化,可点「取消并重试」。",
1173
+ user_cancelled: "更新已被取消,可以重新点「一键更新」再试一次。",
1174
+ npx_cmd_unavailable: "本机找不到可用的安装命令(npx):请先安装 Node.js,或重启一次 dsh web 后重试。",
1175
+ node_missing: "本机没有找到 Node.js:请先安装 Node.js 后重试。",
1176
+ node_too_old: "本机 Node.js 版本过低(需要 20 以上):升级 Node.js 后重试。",
1177
+ npm_unreachable: "连不上软件源:请检查网络后重试(面板会自动换源)。",
1178
+ registry_timeout: "软件源响应超时:稍后重试即可(面板会自动换源)。",
1179
+ platform_unsupported: "当前系统不支持自动更新:请按 README 手动安装。",
1180
+ runtime_install_timeout: "运行环境安装超时:已自动重试,仍失败可点「取消并重试」。",
1181
+ };
1182
+ /** 更新进度的本地兜底(旧 node 半不下发进度字段时用)——放组件外,跨渲染保留。 */
1183
+ var updTrack = { startedAt: 0, lastChangeAt: 0, lastLog: "" };
1184
+
997
1185
  function SelfManageCard() {
998
- var verArr = useState(null); var ver = verArr[0]; var setVer = verArr[1]; // {version, runtimeReady}
1186
+ var verArr = useState(null); var ver = verArr[0]; var setVer = verArr[1]; // {version, channel?, runtimeReady}
999
1187
  var chkArr = useState(null); var chk = chkArr[0]; var setChk = chkArr[1]; // {current, latest, outdated}
1000
1188
  var chkBusyArr = useState(false); var chkBusy = chkBusyArr[0]; var setChkBusy = chkBusyArr[1];
1001
1189
  var upBusyArr = useState(false); var upBusy = upBusyArr[0]; var setUpBusy = upBusyArr[1];
@@ -1005,6 +1193,12 @@ window.__ModuleLoader__.load({
1005
1193
  var doneArr = useState(false); var updated = doneArr[0]; var setUpdated = doneArr[1]; // 本轮已更新完成(提示重启)
1006
1194
  var armArr = useState(false); var armed = armArr[0]; var setArmed = armArr[1]; // 彻底卸载二次确认
1007
1195
  var msgArr = useState(null); var selfMsg = msgArr[0]; var setSelfMsg = msgArr[1]; // {kind, text}
1196
+ // 【0.6.9】更新可见性:进度快照(已用时/无输出/是否卡住)+ 取消按钮状态
1197
+ var progArr = useState(null); var prog = progArr[0]; var setProg = progArr[1];
1198
+ var tickArr = useState(0); var updTick = tickArr[0]; var setTick = tickArr[1]; // 1s 心跳:让「已用时」平滑走字
1199
+ var cxBusyArr = useState(false); var cxBusy = cxBusyArr[0]; var setCxBusy = cxBusyArr[1];
1200
+ var cxOkArr = useState(null); var cancelOk = cxOkArr[0]; var setCancelOk = cxOkArr[1]; // null=未知 true=可用 false=旧宿主无此接口
1201
+ var cxMsgArr = useState(null); var cxMsg = cxMsgArr[0]; var setCxMsg = cxMsgArr[1]; // {kind, text}
1008
1202
 
1009
1203
  var loadVer = useCallback(function () {
1010
1204
  api("/dsh-remote/self").then(function (b) {
@@ -1022,59 +1216,98 @@ window.__ModuleLoader__.load({
1022
1216
  }).finally(function () { setChkBusy(false); });
1023
1217
  }, []);
1024
1218
 
1025
- // 轮询更新日志:点击一键更新后,每 2s 拉一次日志;直到 running=false 视为完成
1026
- var logTimer = useCallback(function (force) {
1027
- if (!force && updating) return;
1028
- api("/dsh-remote/self/update-log").then(function (b) {
1029
- if (b && b.ok) {
1030
- setLog(b.log || "");
1031
- if (!b.running) {
1032
- setUpdating(false);
1033
- setUpdated(true);
1034
- loadVer();
1035
- return;
1036
- }
1037
- }
1038
- setUpdating(true);
1039
- }).catch(function () { setUpdating(false); });
1040
- }, [updating, loadVer]);
1219
+ /**
1220
+ * 处理一次 /self/update-log 响应:刷新日志、进度指标、失败原因与版本;
1221
+ * running=true → 继续跟踪;running=false → 收尾(进程结束 ≠ 更新成功)。
1222
+ */
1223
+ var applyUpdLog = useCallback(function (b) {
1224
+ if (!b || !b.ok) return;
1225
+ var now = Date.now();
1226
+ var nextLog = typeof b.log === "string" ? b.log : "";
1227
+ if (nextLog !== updTrack.lastLog) { updTrack.lastLog = nextLog; updTrack.lastChangeAt = now; }
1228
+ setLog(nextLog);
1229
+ var running = !!b.running;
1230
+ var startedAt = Number(b.startedAt) > 0 ? Number(b.startedAt) : 0;
1231
+ if (running && !updTrack.startedAt) updTrack.startedAt = startedAt || now;
1232
+ if (!running) updTrack.startedAt = 0;
1233
+ var hasIdle = isFinite(Number(b.idleMs));
1234
+ setProg({
1235
+ running: running,
1236
+ at: now,
1237
+ startedAt: startedAt || updTrack.startedAt || 0,
1238
+ elapsedMs: isFinite(Number(b.elapsedMs)) ? Number(b.elapsedMs) : 0,
1239
+ // 旧宿主没有 idleMs → 用「日志最后一次变化的本地时间」兜底
1240
+ idleMs: hasIdle ? Number(b.idleMs) : (running && updTrack.lastChangeAt ? now - updTrack.lastChangeAt : 0),
1241
+ idleThresholdMs: isFinite(Number(b.idleThresholdMs)) ? Number(b.idleThresholdMs) : 0,
1242
+ stalled: b.stalled === true,
1243
+ channel: typeof b.channel === "string" ? b.channel : ""
1244
+ });
1245
+ if (running) { setUpdating(true); return; }
1246
+ setUpdating(false);
1247
+ // 关键:进程结束 ≠ 更新成功。以前这里无条件显示"已更新完成",
1248
+ // 于是 spawn 失败(例如 npx 不可用)时界面只报成功、真实原因躺在日志里 ——
1249
+ // 用户看到的就是"点了一键修复没反应"。现在按失败信息与版本是否变化如实反馈。
1250
+ if (b.failure && b.failure.failCode === "user_cancelled") {
1251
+ // 用户自己点的取消:不是错误,用中性提示,不刷红字
1252
+ setUpdated(false);
1253
+ setSelfMsg({ kind: "warn", text: UPDATE_FAIL_COPY.user_cancelled });
1254
+ } else if (b.failure && b.failure.detail) {
1255
+ setUpdated(false);
1256
+ setSelfMsg({
1257
+ kind: "err",
1258
+ text: "更新失败:" + b.failure.detail + failCodeHint(b.failure.failCode, b.failure.detail) + "(可查看下方日志,或点「一键更新」重试)"
1259
+ });
1260
+ } else {
1261
+ setUpdated(true);
1262
+ }
1263
+ loadVer();
1264
+ doCheck();
1265
+ }, [loadVer, doCheck]);
1266
+
1267
+ /**
1268
+ * fail_code → 人话(未知码返回空串:不编造原因,交给服务端 detail)。
1269
+ * detail 已经说了同一件事时不重复刷一遍(0.6.9 的 update_stalled 就属于这种情况)。
1270
+ */
1271
+ function failCodeHint(code, detail) {
1272
+ var txt = UPDATE_FAIL_COPY[String(code || "")];
1273
+ if (!txt) return "";
1274
+ var key = txt.split(/[,;。]/)[0];
1275
+ if (detail && key && String(detail).indexOf(key) !== -1) return "";
1276
+ return " | " + txt;
1277
+ }
1041
1278
 
1042
- useEffect(function () { loadVer(); doCheck(); }, [loadVer, doCheck]);
1279
+ // 一次性拉取更新状态:点击更新后立刻同步一次;面板打开时也探一次(更新可能是在别处点起的)
1280
+ var logTimer = useCallback(function () {
1281
+ return api("/dsh-remote/self/update-log").then(function (b) {
1282
+ applyUpdLog(b);
1283
+ }).catch(function () { /* 探针失败不影响面板 */ });
1284
+ }, [applyUpdLog]);
1285
+
1286
+ useEffect(function () { loadVer(); doCheck(); logTimer(); }, [loadVer, doCheck, logTimer]);
1287
+ // 更新进行中:每 2s 拉日志+进度;结束(running=false)时由 applyUpdLog 收尾并清掉定时器
1043
1288
  useEffect(function () {
1044
- if (!updating) return;
1045
- var iv = setInterval(function () {
1046
- api("/dsh-remote/self/update-log").then(function (b) {
1047
- if (!b || !b.ok) return;
1048
- setLog(b.log || "");
1049
- if (!b.running) {
1050
- clearInterval(iv);
1051
- setUpdating(false);
1052
- // 关键:进程结束 ≠ 更新成功。以前这里无条件显示"已更新完成",
1053
- // 于是 spawn 失败(例如 npx 不可用)时界面只报成功、真实原因躺在日志里 ——
1054
- // 用户看到的就是"点了一键修复没反应"。现在按失败信息与版本是否变化如实反馈。
1055
- if (b.failure && b.failure.detail) {
1056
- setUpdated(false);
1057
- setSelfMsg({ kind: "err", text: "更新失败:" + b.failure.detail + "(可点「复制诊断信息」,或查看下方日志)" });
1058
- } else {
1059
- setUpdated(true);
1060
- }
1061
- loadVer();
1062
- doCheck();
1063
- }
1064
- }).catch(function () {});
1065
- }, 2000);
1289
+ if (!updating) return undefined;
1290
+ var iv = setInterval(function () { logTimer(); }, 2000);
1291
+ return function () { clearInterval(iv); };
1292
+ }, [updating, logTimer]);
1293
+ // 「已用时」走字:只在更新中开 1s 心跳(尊重 reduced-motion 也不影响,纯文本更新)
1294
+ useEffect(function () {
1295
+ if (!updating) return undefined;
1296
+ var iv = setInterval(function () { setTick(function (v) { return v + 1; }); }, 1000);
1066
1297
  return function () { clearInterval(iv); };
1067
- }, [updating, loadVer, doCheck]);
1298
+ }, [updating]);
1299
+ // 注:updTick 只用于触发重渲染(setTick 的引用是稳定的),不进依赖列表
1068
1300
 
1069
1301
  var doUpdate = function () {
1070
1302
  setUpBusy(true);
1071
1303
  setUpdated(false);
1072
1304
  setSelfMsg(null);
1305
+ setCxMsg(null);
1073
1306
  post("/dsh-remote/self/update", {}).then(function (b) {
1074
1307
  if (b && b.ok) {
1075
- setSelfMsg({ kind: "ok", text: "更新已在后台开始,正在下载安装…(本页会实时显示进度日志)" });
1308
+ setSelfMsg({ kind: "ok", text: "更新已在后台开始,正在下载安装…(本页会实时显示进度日志、已用时与通道)" });
1076
1309
  setUpdating(true);
1077
- logTimer(true);
1310
+ logTimer();
1078
1311
  } else {
1079
1312
  var detail = String((b && (b.detail || b.error)) || "更新启动失败");
1080
1313
  // 另一种常见情况:node 半返回 ok:false + “已有更新在进行中” → 转为跟踪进度而非报错
@@ -1082,7 +1315,7 @@ window.__ModuleLoader__.load({
1082
1315
  if (lb && lb.ok && lb.running) {
1083
1316
  setSelfMsg({ kind: "ok", text: "检测到已有一次更新正在进行,正在跟踪进度…" });
1084
1317
  setUpdating(true);
1085
- logTimer(true);
1318
+ logTimer();
1086
1319
  } else {
1087
1320
  setSelfMsg({ kind: "err", text: detail });
1088
1321
  }
@@ -1093,6 +1326,40 @@ window.__ModuleLoader__.load({
1093
1326
  }).finally(function () { setUpBusy(false); });
1094
1327
  };
1095
1328
 
1329
+ /**
1330
+ * 取消更新:POST /dsh-remote/self/update/cancel(结束挂死的更新进程 + 清标记)。
1331
+ * retry=true → 取消成功后立刻重新点一次「一键更新」(即按钮上的「取消并重试」)。
1332
+ * 旧 node 半没有这个路由 → 404 → 隐藏按钮并给一句能照做的话(不把用户留在死胡同)。
1333
+ */
1334
+ var doCancelUpdate = function (retry) {
1335
+ setCxBusy(true);
1336
+ setCxMsg(null);
1337
+ post("/dsh-remote/self/update/cancel", {}).then(function (b) {
1338
+ if (!b || !b.ok) throw new Error((b && (b.detail || b.error)) || "取消失败");
1339
+ setCancelOk(true);
1340
+ setUpdating(false);
1341
+ setUpdated(false);
1342
+ setProg(null);
1343
+ updTrack.startedAt = 0;
1344
+ setCxMsg({
1345
+ kind: b.cleared === false ? "warn" : "ok",
1346
+ text: b.cleared === false
1347
+ ? "取消请求已发出,但更新标记没能清掉:请刷新页面;若仍显示「正在更新」,重启一次 dsh web 即可。"
1348
+ : ("已取消本次更新" + (b.killed === true ? "(更新进程已结束)" : "") + (retry ? ",正在重新开始…" : ",可以重新点「一键更新」再试一次。"))
1349
+ });
1350
+ loadVer();
1351
+ if (retry && b.cleared !== false) doUpdate();
1352
+ }).catch(function (e) {
1353
+ if (e && e.status === 404) {
1354
+ // 旧宿主:没有取消接口 → 永久隐藏该按钮,只留人话提示
1355
+ setCancelOk(false);
1356
+ setCxMsg({ kind: "warn", text: "当前插件版本还不支持一键取消更新:请刷新页面;若仍卡在「正在更新」,重启一次 dsh web 后重新点「一键更新」即可。" });
1357
+ } else {
1358
+ setCxMsg({ kind: "err", text: "取消失败:" + ((e && e.message) || "未知错误") + "(更新进程可能仍在运行;可稍后重试,或重启一次 dsh web)" });
1359
+ }
1360
+ }).finally(function () { setCxBusy(false); });
1361
+ };
1362
+
1096
1363
  var doUninstall = function () {
1097
1364
  if (!armed) { setArmed(true); return; }
1098
1365
  setUnBusy(true);
@@ -1105,7 +1372,7 @@ window.__ModuleLoader__.load({
1105
1372
  // 优先展示服务端 detail(含 bridge 自启动/配置目录的逐项清理结果与重启提示);
1106
1373
  // 兜底文案同样说明 bridge 自启动服务与本地配置目录会一并移除/清空
1107
1374
  var detail = b && b.detail ? String(b.detail) : "";
1108
- setSelfMsg({ kind: "ok", text: detail || "已彻底卸载:插件引用、bridge 自启动服务与本地配置目录(账号/密钥/运行时等)已一并移除并清空。请重启 dsh web 后完全生效(本栏目将消失);如需再次使用,在插件市场重新安装即可。" });
1375
+ setSelfMsg({ kind: "ok", text: detail || "已彻底卸载:插件引用、bridge 自启动服务与本地配置目录(账号/密钥/运行时等)已一并移除并清空。请重启 dsh web 后完全生效(本栏目将消失);如需再次使用,在插件市场搜索「dsh-remote-web」重新安装,或执行 npx @mrrisega/dsh-remote。" });
1109
1376
  } else {
1110
1377
  setArmed(false);
1111
1378
  setSelfMsg({ kind: "err", text: "卸载失败:" + ((b && (b.error || b.detail)) || "未知错误") });
@@ -1119,19 +1386,67 @@ window.__ModuleLoader__.load({
1119
1386
  var outdated = !!(chk && chk.outdated && chk.latest && chk.latest !== chk.current);
1120
1387
  var currentV = (ver && ver.version) || (chk && chk.current) || "…";
1121
1388
  var runtimeReady = ver ? !!ver.runtimeReady : null;
1389
+ // 通道:GET /self 的 channel 优先(字段可能不存在 → 不渲染任何通道 chip,旧宿主照常工作)
1390
+ var channel = (ver && typeof ver.channel === "string" && ver.channel) || (prog && prog.channel) || "";
1391
+ // 进度指标(字段缺失时用本地观测兜底):已用时 / 无输出时长 / 是否卡住
1392
+ var progRunning = !!(prog && prog.running);
1393
+ var elapsedBase = prog && prog.elapsedMs > 0 ? prog.elapsedMs
1394
+ : (prog && prog.startedAt ? Math.max(0, Date.now() - prog.startedAt) : 0);
1395
+ var elapsedShown = prog && prog.at ? elapsedBase + Math.max(0, Date.now() - prog.at) : elapsedBase;
1396
+ var idleShown = prog ? prog.idleMs + (prog.at ? Math.max(0, Date.now() - prog.at) : 0) : 0;
1397
+ var cancelAfterMs = prog && prog.idleThresholdMs > 0 ? prog.idleThresholdMs : UPDATE_CANCEL_IDLE_MS;
1398
+ // 「卡住」只采信服务端判定(stalled=true);旧宿主没有该字段时退化为下面的「无输出」灰字提示,
1399
+ // 不自己编一个「卡住」结论(避免误报)。
1400
+ var stalled = !!(prog && prog.stalled);
1401
+ var canCancel = updating && cancelOk !== false && ((prog && prog.stalled) || idleShown >= cancelAfterMs);
1402
+ var idleHint = progRunning && !stalled && idleShown >= UPDATE_IDLE_HINT_MS;
1122
1403
 
1123
1404
  return h("div", { className: "dru-card", style: { marginTop: 2 } },
1124
1405
  h("h3", null, "🔄 版本与更新"),
1125
- h("div", { className: "dru-status-line" },
1126
- h("span", null, "插件版本 v" + currentV),
1406
+ // ① 版本行:版本号(大字)+ 通道 chip + 新旧状态
1407
+ h("div", { className: "dru-ver-head" },
1408
+ h("span", { className: "dru-ver-num" }, "插件 v" + currentV),
1409
+ channel ? h("span", { className: "dru-ver-chip", title: "当前更新通道:" + channel }, "通道 " + updateChannelText(channel)) : null,
1127
1410
  chk === null && chkBusy ? h("span", { className: "dru-meta", style: { margin: 0 } }, "(检查新版本中…)") : null,
1128
1411
  chk && outdated
1129
1412
  ? h("span", { className: "dru-ver-badge dru-ver-badge-new" }, "发现新版本 v" + chk.latest)
1130
1413
  : chk && !outdated ? h("span", { className: "dru-ver-badge dru-ver-badge-ok" }, "已是最新版本") : null
1131
1414
  ),
1132
1415
  h("div", { className: "dru-meta" },
1133
- runtimeReady === false ? "⚠ 桌面运行环境缺失(点击下方「一键更新」会自动补全并启动)" : runtimeReady === true ? "桌面运行环境正常" : "读取运行环境中…"
1416
+ runtimeReady === false
1417
+ ? "⚠ 后台服务运行环境缺失(点下方「一键更新」会自动补齐并启动,不用手动装)"
1418
+ : runtimeReady === true ? "后台服务运行环境正常" : "正在读取运行环境…"
1134
1419
  ),
1420
+ // ② 更新进行中:进度指标(已用时 / 距上次输出)+ 卡住告警(0.6.9 看门狗冻结契约)
1421
+ updating || progRunning
1422
+ ? h("div", { className: "dru-ver-metrics", role: "status" },
1423
+ h("div", { className: "dru-ver-metric" },
1424
+ h("span", null, "更新状态"),
1425
+ h("span", null, h("span", { className: "dru-spin", "aria-hidden": "true" }), " 正在进行")
1426
+ ),
1427
+ h("div", { className: "dru-ver-metric" }, h("span", null, "已用时"), h("span", null, fmtDuration(elapsedShown))),
1428
+ h("div", { className: "dru-ver-metric" },
1429
+ h("span", null, "距上次输出"),
1430
+ h("span", null, idleShown < 1500 ? "刚刚" : fmtDuration(idleShown))
1431
+ ),
1432
+ channel ? h("div", { className: "dru-ver-metric" }, h("span", null, "更新通道"), h("span", null, channel)) : null
1433
+ )
1434
+ : null,
1435
+ stalled
1436
+ ? h("div", { className: "dru-stall", role: "alert" },
1437
+ h("b", null, "疑似卡住"),
1438
+ "(已 " + Math.round(idleShown / 1000) + " 秒无输出),正在自动换源重试…",
1439
+ "已经等了 " + fmtDuration(elapsedShown) + ";若长时间没有变化,可点下方「取消并重试」。",
1440
+ cancelOk === false
1441
+ ? h("div", { style: { marginTop: 6 } },
1442
+ "当前插件版本还不支持一键取消:可刷新页面;若仍卡在「正在更新」,重启一次 dsh web 后重新点「一键更新」即可。")
1443
+ : null
1444
+ )
1445
+ : idleHint
1446
+ ? h("div", { className: "dru-hint", style: { marginTop: 8 } },
1447
+ "已 " + Math.round(idleShown / 1000) + " 秒没有新输出(总计 " + fmtDuration(elapsedShown) + "):安装过程偶尔会安静一会儿,超过 " + Math.round(cancelAfterMs / 1000) + " 秒没变化会出现「取消并重试」。")
1448
+ : null,
1449
+ // ③ 操作区(主操作 / 检查 / 取消并重试 / 彻底卸载)
1135
1450
  h("div", { className: "dru-actions", style: { marginTop: 10 } },
1136
1451
  outdated
1137
1452
  ? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: upBusy || unBusy || chkBusy || updating, onClick: doUpdate },
@@ -1139,6 +1454,15 @@ window.__ModuleLoader__.load({
1139
1454
  : h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: upBusy || unBusy || chkBusy || updating, onClick: doUpdate },
1140
1455
  updating ? "正在更新…" : (ver && !ver.runtimeReady) ? "安装并启动(一键修复)" : "重新检查 / 修复"),
1141
1456
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: chkBusy || updating || upBusy, onClick: doCheck }, chkBusy ? "检查中…" : "检查更新"),
1457
+ canCancel
1458
+ ? h("button", {
1459
+ type: "button",
1460
+ className: "dru-btn dru-btn-danger",
1461
+ disabled: cxBusy || upBusy,
1462
+ title: "结束卡住的更新进程并清除更新标记,然后重新开始一次",
1463
+ onClick: function () { doCancelUpdate(true); }
1464
+ }, cxBusy ? "取消中…" : "取消并重试")
1465
+ : null,
1142
1466
  h("button", {
1143
1467
  type: "button",
1144
1468
  className: "dru-btn dru-btn-danger",
@@ -1147,15 +1471,17 @@ window.__ModuleLoader__.load({
1147
1471
  onClick: doUninstall
1148
1472
  }, unBusy ? "卸载中…" : armed ? "⚠ 再点一次确认彻底卸载" : "彻底卸载")
1149
1473
  ),
1474
+ cxMsg ? h("div", { className: "dru-msg dru-msg-" + cxMsg.kind }, cxMsg.text) : null,
1150
1475
  updated
1151
1476
  ? h("div", { className: "dru-msg dru-msg-ok" },
1152
- "✅ 更新已完成,最新代码已就位。请", h("strong", null, "重启 dsh web"), "后生效;桌面 bridge 会随系统自启自动运行新版本。")
1477
+ "✅ 更新已完成,最新代码已就位。请", h("strong", null, "重启 dsh web"), "后生效;后台服务会随系统自启自动运行新版本。")
1153
1478
  : null,
1479
+ // ④ 更新日志(失败时同样靠近上面的错误提示)
1154
1480
  log ? h("div", { className: "dru-up-log", title: "更新日志(尾部)" }, log) : null,
1155
1481
  selfMsg ? h("div", { className: "dru-msg dru-msg-" + selfMsg.kind }, selfMsg.text) : null,
1156
1482
  h("div", { className: "dru-hint", style: { marginTop: 8 } },
1157
- armed ? "⚠ 再次点击后即开始彻底卸载:① 移除 dsh web 配置中的插件引用与本地文件;② 停止并移除 bridge 自启动服务(macOS com.dshremote.bridge / Linux dsh-bridge / Windows 任务计划程序 dsh-remote-bridge)并结束残留进程;③ 清空本地配置目录(~/.dsh-remote:账号、设备密钥、固化运行时等)。此操作不可撤销,如需再次使用请在插件市场重新安装。" :
1158
- "检测新版、一键在线更新、彻底卸载都在本卡片完成。")
1483
+ armed ? "⚠ 再次点击后即开始彻底卸载:① 移除 dsh web 配置中的插件引用与本地文件;② 停止并移除 bridge 自启动服务(即让「后台服务」不再开机自启:macOS com.dshremote.bridge / Linux dsh-bridge / Windows 任务计划程序 dsh-remote-bridge)并结束残留进程;③ 清空本地配置目录(~/.dsh-remote:账号、设备密钥、固化运行时等)。此操作不可撤销,如需再次使用:在插件市场搜索「dsh-remote-web」重新安装,或执行 npx @mrrisega/dsh-remote。" :
1484
+ "检测新版、一键在线更新、彻底卸载都在本卡片完成;更新卡住时会出现「取消并重试」。")
1159
1485
  );
1160
1486
  }
1161
1487
 
@@ -1189,6 +1515,8 @@ window.__ModuleLoader__.load({
1189
1515
  var viewArr = useState("home"); var view = viewArr[0]; var setView = viewArr[1]; // home | feedback | invite
1190
1516
  var busyArr = useState(""); var busy = busyArr[0]; var setBusy = busyArr[1];
1191
1517
  var msgArr = useState(null); var message = msgArr[0]; var setMessage = msgArr[1];
1518
+ // 注:邀请区复制反馈已迁到 viewstate 末端的 inviteCopied(0.6.9);这个 hook 保留占位,
1519
+ // 避免后续所有 hook 序号前移(本组件大量 useCallback/useEffect 依赖稳定序号)。
1192
1520
  var copiedArr = useState(false); var copied = copiedArr[0]; var setCopied = copiedArr[1];
1193
1521
 
1194
1522
  // 我的信息(个人中心)数据
@@ -1253,6 +1581,13 @@ window.__ModuleLoader__.load({
1253
1581
  var connArr = useState(null); var conn = connArr[0]; var setConn = connArr[1];
1254
1582
  var copiedDiagArr = useState(false); var copiedDiag = copiedDiagArr[0]; var setCopiedDiag = copiedDiagArr[1];
1255
1583
 
1584
+ // ── 🎯 邀请视图(0.6.9 重排)状态:放在全部既有字段之后,保持既有 hook 序号不变 ──
1585
+ var invLoadArr = useState(false); var inviteLoading = invLoadArr[0]; var setInviteLoading = invLoadArr[1]; // 记录/规则读取中
1586
+ var invErrArr = useState(null); var inviteErr = invErrArr[0]; var setInviteErr = invErrArr[1]; // 记录读取失败原因(≠空记录)
1587
+ var invRulesArr = useState(false); var inviteRulesOpen = invRulesArr[0]; var setInviteRulesOpen = invRulesArr[1]; // 活动规则默认折叠
1588
+ var invCopyArr = useState(""); var inviteCopied = invCopyArr[0]; var setInviteCopied = invCopyArr[1]; // "" | "code" | "link"(复制成功反馈)
1589
+ var invCopyErrArr = useState(null); var inviteCopyErr = invCopyErrArr[0]; var setInviteCopyErr = invCopyErrArr[1]; // 复制失败原因(就近提示)
1590
+
1256
1591
  var refresh = useCallback(function () {
1257
1592
  setBusy("status");
1258
1593
  api("/dsh-remote/status").then(function (body) {
@@ -1417,12 +1752,15 @@ window.__ModuleLoader__.load({
1417
1752
 
1418
1753
  var copyKeyUrl = function () {
1419
1754
  if (!(akey && akey.url)) return;
1420
- try {
1421
- navigator.clipboard.writeText(akey.url).then(function () {
1422
- setCopiedKey(true);
1423
- setTimeout(function () { setCopiedKey(false); }, 1500);
1424
- });
1425
- } catch (e) {}
1755
+ // 统一走 copyText:剪贴板不可用(http 非安全上下文)时退回 execCommand;都失败就给一句人话
1756
+ copyText(akey.url).then(function (done) {
1757
+ if (!done) {
1758
+ setAkeyMsg({ kind: "err", text: "复制失败:浏览器可能限制了剪贴板权限,请手动选中上面的链接复制。" });
1759
+ return;
1760
+ }
1761
+ setCopiedKey(true);
1762
+ later(function () { setCopiedKey(false); }, 2000);
1763
+ });
1426
1764
  };
1427
1765
  /** 「直接打开」:浏览器新标签打开一次性访问地址(打开即扫码/点击进入)。 */
1428
1766
  var openKeyUrl = function () {
@@ -1689,8 +2027,8 @@ window.__ModuleLoader__.load({
1689
2027
  text: "状态读取失败",
1690
2028
  detail: "本机状态接口连续 " + connFailRef.v + " 次请求失败:"
1691
2029
  + ((e && e.message) ? e.message : String(e))
1692
- + "(面板读不到 bridge 状态,扫码/远程访问都不会推进)。可点「重试」,或刷新页面;若持续失败请把诊断信息发给客服。",
1693
- error: { code: "status_unreachable", message: "无法读取本机 bridge 状态(/dsh-remote/bridge-status 请求失败)" },
2030
+ + "(面板读不到后台服务状态,扫码/远程访问都不会推进)。可点「重试」,或刷新页面;若持续失败请把诊断信息发给客服。",
2031
+ error: { code: "status_unreachable", message: "无法读取本机后台服务状态(/dsh-remote/bridge-status 请求失败)" },
1694
2032
  retryable: true,
1695
2033
  attempts: connFailRef.v,
1696
2034
  });
@@ -1714,12 +2052,15 @@ window.__ModuleLoader__.load({
1714
2052
  var copyDiagnostics = function () {
1715
2053
  var text = (connInfo && connInfo.diagnostics) || "";
1716
2054
  if (!text) return;
1717
- try {
1718
- navigator.clipboard.writeText(text).then(function () {
1719
- setCopiedDiag(true);
1720
- later(function () { setCopiedDiag(false); }, 1500);
1721
- });
1722
- } catch (e) { /* 剪贴板不可用:文案里已给出日志路径,用户可自行查看 */ }
2055
+ copyText(text).then(function (done) {
2056
+ if (!done) {
2057
+ // 剪贴板不可用:文案里已给出日志路径,用户可自行查看
2058
+ setMsg("warn", "复制失败:浏览器可能限制了剪贴板权限;可展开下方日志路径自行查看。");
2059
+ return;
2060
+ }
2061
+ setCopiedDiag(true);
2062
+ later(function () { setCopiedDiag(false); }, 2000);
2063
+ });
1723
2064
  };
1724
2065
 
1725
2066
  function setMsg(kind, text) { setMessage({ kind: kind, text: text }); }
@@ -1738,7 +2079,7 @@ window.__ModuleLoader__.load({
1738
2079
  setSt(cfg);
1739
2080
  var br = cfg && cfg.bridgeRestart;
1740
2081
  if (br && br.ok === false && br.status !== "running" && br.status !== "provisioning" && br.status !== "skipped") {
1741
- setMsg("warn", okText + ";但后台服务(bridge)未能自动重启:"
2082
+ setMsg("warn", okText + ";但后台服务未能自动重启:"
1742
2083
  + ((br.detail || br.status) || "未知原因")
1743
2084
  + " —— 新账号可能收不到这台设备,请点下方「重启服务」或重启一次 dsh web。");
1744
2085
  return;
@@ -1944,7 +2285,7 @@ window.__ModuleLoader__.load({
1944
2285
  post(start ? "/dsh-remote/start" : "/dsh-remote/stop")
1945
2286
  .then(function (body) {
1946
2287
  setSt(body);
1947
- setMsg(body.ok ? "ok" : "err", body.ok ? (start ? "✅ bridge 已启动" + (body.pid ? " (pid=" + body.pid + ")" : "") : "bridge 已停止") : (body.detail || body.status || "操作失败"));
2288
+ setMsg(body.ok ? "ok" : "err", body.ok ? (start ? "✅ 后台服务已启动" + (body.pid ? " (pid=" + body.pid + ")" : "") : "后台服务已停止") : (body.detail || body.status || "操作失败"));
1948
2289
  })
1949
2290
  .catch(function (e) { setMsg("err", (start ? "启动" : "停止") + "失败: " + e.message); })
1950
2291
  .finally(function () { setBusy(""); });
@@ -2011,23 +2352,36 @@ window.__ModuleLoader__.load({
2011
2352
  fbClearThreads();
2012
2353
  setPwdOpen(false);
2013
2354
  setSt(body); setSelfHost(""); setLocalKey("");
2014
- setMsg(body.ok ? "ok" : "err", body.ok ? "✅ 已切换到自建服务,bridge 已重启" : (body.error || (body.body && body.body.error) || "保存失败"));
2355
+ setMsg(body.ok ? "ok" : "err", body.ok ? "✅ 已切换到自建服务,后台服务已重启" : (body.error || (body.body && body.body.error) || "保存失败"));
2015
2356
  })
2016
2357
  .catch(function (e) { setMsg("err", "保存失败: " + e.message); })
2017
2358
  .finally(function () { setBusy(""); });
2018
2359
  };
2019
2360
 
2361
+ /**
2362
+ * 拉取邀请视图数据:公共配置(邀请规则/试用天数)+ 我的邀请记录。
2363
+ * 【0.6.9 修正】记录拉取失败**不再静默当成"没有记录"**:否则用户会以为
2364
+ * "好友明明装好了却没记上"。失败时记下原因,由列表位置给出「重试」。
2365
+ */
2020
2366
  var loadInvite = function () {
2021
- setBusy("invite");
2367
+ setInviteLoading(true);
2368
+ setInviteErr(null);
2369
+ setInviteCopyErr(null);
2022
2370
  api("/dsh-remote/remote-url").then(function (b) {
2023
2371
  var pubBody = (b && b.publicConfig) || {};
2024
2372
  setPub(pubBody);
2025
- }).catch(function () {});
2373
+ }).catch(function () { /* 规则取不到 → 用默认 n=3/days=15 兜底,不打扰 */ });
2026
2374
  api("/dsh-remote/invite-records").then(function (b) {
2027
- if (b && b.ok) setInviteData({ records: b.records || [], rewards: b.rewards || [] });
2028
- else setInviteData({ records: [], rewards: [] });
2029
- }).catch(function () { setInviteData({ records: [], rewards: [] }); })
2030
- .finally(function () { setBusy(""); });
2375
+ if (b && b.ok) {
2376
+ setInviteData({ records: Array.isArray(b.records) ? b.records : [], rewards: Array.isArray(b.rewards) ? b.rewards : [] });
2377
+ } else {
2378
+ setInviteData({ records: [], rewards: [] });
2379
+ setInviteErr(String((b && (b.error || b.detail)) || "服务端未返回邀请记录"));
2380
+ }
2381
+ }).catch(function (e) {
2382
+ setInviteData({ records: [], rewards: [] });
2383
+ setInviteErr((e && e.message) ? String(e.message) : "网络错误");
2384
+ }).finally(function () { setInviteLoading(false); });
2031
2385
  };
2032
2386
 
2033
2387
  var field = function (label, inputEl) {
@@ -2035,6 +2389,20 @@ window.__ModuleLoader__.load({
2035
2389
  };
2036
2390
  var input = function (attrs) { return h("input", Object.assign({ className: "dru-input", type: "text" }, attrs)); };
2037
2391
  var card = function (title, children) { return h("div", { className: "dru-card" }, title ? h("h3", null, title) : null, children); };
2392
+ /**
2393
+ * 轻量邀请入口(0.6.9):额度将尽 / 连接偏慢时用的**文字链**层级入口——
2394
+ * 与主 CTA(升级 PRO / 重试)并列但不抢焦点,不弹窗、不打断。
2395
+ */
2396
+ function renderInviteLink(label, title) {
2397
+ return h("button", {
2398
+ type: "button",
2399
+ className: "dru-linkbtn",
2400
+ style: { margin: "0 4px" },
2401
+ title: title || label,
2402
+ disabled: busy !== "",
2403
+ onClick: function () { setView("invite"); loadInvite(); }
2404
+ }, label);
2405
+ }
2038
2406
 
2039
2407
  // ---------- 我的信息(个人中心) ----------
2040
2408
  function fmtDate(ts) {
@@ -2066,6 +2434,17 @@ window.__ModuleLoader__.load({
2066
2434
  else if (source === "trial") planText = "试用 PRO 会员 · 到期 " + fmtDate(a.trial_expires_at);
2067
2435
  else if (endsAt) planText = plan === "pro_max" ? "Pro Max 会员 · 到期 " + fmtDate(endsAt) : "PRO 会员 · 到期 " + fmtDate(endsAt);
2068
2436
  else planText = plan === "pro_max" ? "Pro Max 长期会员" : "PRO 长期会员";
2437
+ // 【0.6.9】额度将尽时的轻量邀请入口:只依据服务端已下发的额度字段判断
2438
+ // · limit_enabled 且 percent ≥ 80(接近用尽)
2439
+ // · limit_enabled 但没下发 max_mbps(文案已显示「已限速」=正在被限速)
2440
+ // 判断不了(quota 缺失、不限量、会员)→ 不显示,绝不误报。
2441
+ var quotaPctNum = Number(quotaPct);
2442
+ var quotaTight = !!(quota && quota.limit_enabled && !isMember &&
2443
+ ((isFinite(quotaPctNum) && quotaPctNum >= 80) || !quota.max_mbps));
2444
+ // 邀请规则(公共配置下发;缺省时用与邀请页一致的兜底值,不编造)
2445
+ var invRule = (pub && pub.invite_rule) || {};
2446
+ var invRuleN = Number(invRule.n) > 0 ? Number(invRule.n) : 3;
2447
+ var invRuleDays = Number(invRule.days) > 0 ? Number(invRule.days) : 15;
2069
2448
  return h("div", null,
2070
2449
  h("div", { className: "dru-user" },
2071
2450
  h("div", { className: "dru-avatar" }, (st.config.phone || "D").charAt(0).toUpperCase()),
@@ -2082,7 +2461,7 @@ window.__ModuleLoader__.load({
2082
2461
  h("div", { className: "dru-actions", style: { marginTop: 10 } },
2083
2462
  h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { display: "inline-flex", alignItems: "center" }, disabled: busy !== "", title: "升级/续费(带登录态打开)", onClick: openUpgradeAuth },
2084
2463
  busy === "upgrade" ? "生成链接中…" : (!isMember ? "🚀 升级 PRO" : source === "trial" ? "🚀 转正式 PRO" : "🔄 续费会员")),
2085
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("invite"); loadInvite(); } }, "🎯 邀请好友赚会员"),
2464
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("invite"); loadInvite(); } }, "🎯 邀请好友得会员"),
2086
2465
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("feedback"); } }, "💬 用户反馈"),
2087
2466
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: togglePwdForm }, pwdOpen ? "收起修改密码" : "🔒 修改密码"),
2088
2467
  h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { doLogout(false); } }, "退出登录")
@@ -2090,6 +2469,13 @@ window.__ModuleLoader__.load({
2090
2469
  h("div", { className: "dru-hint", style: { marginTop: 8 } },
2091
2470
  "升级/续费以带登录态方式打开:点击后生成一次性访问链接并直接跳转,无需重新登录。" +
2092
2471
  (endsAt && isMember ? "到期后如需继续使用会员权益,请在到期前续费。" : "")),
2472
+ // 额度将尽 → 轻量邀请入口(文字链层级,不抢「升级 PRO」主 CTA)
2473
+ quotaTight
2474
+ ? h("div", { className: "dru-hint", style: { marginTop: 6 } },
2475
+ "免费额度快用完了?也可以先",
2476
+ renderInviteLink("🎯 邀请好友得会员", "邀请好友获得 PRO 会员奖励"),
2477
+ "——每邀请满 " + invRuleN + " 位好友,得 " + invRuleDays + " 天 PRO 会员(详情见邀请页)。")
2478
+ : null,
2093
2479
  pwdOpen ? renderResetPwdForm(false) : null
2094
2480
  );
2095
2481
  }
@@ -2134,46 +2520,240 @@ window.__ModuleLoader__.load({
2134
2520
  );
2135
2521
  }
2136
2522
 
2137
- // ---------- 邀请视图 ----------
2523
+ // ---------- 🎯 邀请视图(0.6.9 重排:奖励大字 → 进度 → 邀请码/链接一键复制 → 折叠规则 → 记录) ----------
2524
+ /** 体面空态:图标 + 标题 + 说明 + 可选操作(替代过去"一行灰字了事")。 */
2525
+ function emptyState(icon, title, sub, actions) {
2526
+ return h("div", { className: "dru-empty" },
2527
+ h("div", { className: "dru-empty-icon", "aria-hidden": "true" }, icon),
2528
+ h("div", { className: "dru-empty-title" }, title),
2529
+ sub ? h("div", { className: "dru-empty-sub" }, sub) : null,
2530
+ actions && actions.length ? h("div", { className: "dru-empty-actions" }, actions) : null
2531
+ );
2532
+ }
2533
+ /**
2534
+ * 一键复制按钮:成功后按钮自身变绿并显示「已复制」2 秒;失败则在按钮旁(同一个 .dru-copy-row)
2535
+ * 给出红字提示 —— 错误靠近出错位置,不让用户以为"点了没反应"。
2536
+ */
2537
+ function copyButton(key, text, label) {
2538
+ var ok = inviteCopied === key;
2539
+ return h("button", {
2540
+ type: "button",
2541
+ className: "dru-copy-btn" + (ok ? " copied" : ""),
2542
+ disabled: !text,
2543
+ "aria-label": label,
2544
+ onClick: function () {
2545
+ copyText(text).then(function (done) {
2546
+ if (!done) {
2547
+ setInviteCopied("");
2548
+ setInviteCopyErr({ kind: key, text: "复制失败:请手动选中上面的文字复制(浏览器可能限制了剪贴板权限)" });
2549
+ return;
2550
+ }
2551
+ setInviteCopyErr(null);
2552
+ setInviteCopied(key);
2553
+ later(function () { setInviteCopied(function (cur) { return cur === key ? "" : cur; }); }, 2000);
2554
+ });
2555
+ }
2556
+ }, ok ? "✅ 已复制" : label);
2557
+ }
2558
+ /** 邀请码 / 邀请链接的一行(标签 + 值 + 复制按钮 + 就近错误提示)。 */
2559
+ function copyRow(label, value, kind, hint) {
2560
+ var isCode = kind === "code";
2561
+ return h("div", { className: "dru-copy-row" },
2562
+ h("div", { className: "dru-copy-label" }, label),
2563
+ h("div", { className: "dru-copy-box" },
2564
+ h("span", { className: isCode ? "dru-code-val" : "dru-link-val" }, value),
2565
+ copyButton(kind, value, isCode ? "复制邀请码" : "复制邀请链接")
2566
+ ),
2567
+ hint ? h("div", { className: "dru-invite-sub", style: { marginTop: 6 } }, hint) : null,
2568
+ // 复制失败提示紧贴这一次操作的行(错误靠近出错位置)
2569
+ inviteCopyErr && inviteCopyErr.kind === kind
2570
+ ? h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 6 } }, inviteCopyErr.text)
2571
+ : null
2572
+ );
2573
+ }
2574
+ /** 邀请记录里的好友展示名:只用接口给的 invitee_phone(掩码),缺字段时降级,不编造。 */
2575
+ function inviteeName(r) {
2576
+ var phone = maskPhoneLike(r && r.invitee_phone);
2577
+ if (phone) return phone;
2578
+ var dev = r && r.device_id ? String(r.device_id) : "";
2579
+ if (dev) return "好友(设备 " + (dev.length > 6 ? dev.slice(-6) : dev) + ")";
2580
+ return "好友";
2581
+ }
2582
+ /** 一条邀请记录:谁 / 什么时候 / 是否已生效(时间或状态字段缺失时如实标注,不编造)。 */
2583
+ function renderInviteRecords(records, ruleN) {
2584
+ return h("div", null, records.map(function (r, i) {
2585
+ var ts = r && (r.created_at || r.bound_at || r.at);
2586
+ // 记录来自「有效邀请」接口:默认即已生效;若服务端给了 reward_granted:false → 标"待发放"
2587
+ var pending = r && r.reward_granted === false;
2588
+ return h("div", { key: (r && r.id != null ? String(r.id) : "rec-" + i), className: "dru-rec" },
2589
+ h("div", { className: "dru-rec-top" },
2590
+ h("span", { className: "dru-rec-who" }, inviteeName(r)),
2591
+ h("span", { className: "dru-rec-tag " + (pending ? "dru-rec-tag-wait" : "dru-rec-tag-ok") },
2592
+ pending ? "待发放奖励" : "已生效"),
2593
+ h("span", { className: "dru-rec-when" }, ts ? fmtDate(ts) : "时间未知")
2594
+ ),
2595
+ h("div", { className: "dru-rec-sub" },
2596
+ pending
2597
+ ? "好友已完成设备绑定,奖励按规则结算(每满 " + ruleN + " 位发放一次)。"
2598
+ : "好友已完成电脑端安装并绑定设备,已计入你的有效邀请。")
2599
+ );
2600
+ }));
2601
+ }
2602
+ /** 已到账奖励:每次奖励 = rule_days 天 PRO(时间字段缺失时只显示天数)。 */
2603
+ function renderInviteRewards(rewards, ruleDays) {
2604
+ if (!rewards.length) return null;
2605
+ return h("div", { className: "dru-copy-row" },
2606
+ h("div", { className: "dru-copy-label" }, "已到账奖励(" + rewards.length + " 次)"),
2607
+ h("div", null, rewards.map(function (r, i) {
2608
+ var days = r && (r.rule_days != null ? r.rule_days : ruleDays);
2609
+ var ts = r && (r.created_at || r.at);
2610
+ return h("div", { key: (r && r.id != null ? String(r.id) : "rw-" + i), className: "dru-rec" },
2611
+ h("div", { className: "dru-rec-top" },
2612
+ h("span", { className: "dru-rec-who" }, "+" + (days != null ? days : ruleDays) + " 天 PRO 会员"),
2613
+ h("span", { className: "dru-rec-tag dru-rec-tag-ok" }, "已到账"),
2614
+ h("span", { className: "dru-rec-when" }, ts ? fmtDate(ts) : "时间未提供")
2615
+ )
2616
+ );
2617
+ }))
2618
+ );
2619
+ }
2620
+ /** 活动规则:默认折叠,点开才显示(层级清晰,不再和奖励说明挤在一堆小字里)。 */
2621
+ function renderInviteRules(n, days) {
2622
+ return h("div", { className: "dru-card" },
2623
+ h("button", {
2624
+ type: "button",
2625
+ className: "dru-disclose",
2626
+ "aria-expanded": inviteRulesOpen ? "true" : "false",
2627
+ onClick: function () { setInviteRulesOpen(!inviteRulesOpen); }
2628
+ },
2629
+ h("span", null, "活动规则"),
2630
+ h("span", { className: "dru-disclose-caret" }, inviteRulesOpen ? "收起 ▲" : "展开 ▼")
2631
+ ),
2632
+ inviteRulesOpen
2633
+ ? h("ol", { className: "dru-rules" }, [
2634
+ h("li", null, "好友通过你的邀请链接注册(或在注册时填写你的邀请码),即与你绑定邀请关系。"),
2635
+ h("li", null, "好友在这台电脑上装好后台服务并成功登录后,才计入 1 位", h("b", null, "有效邀请"), ";同一位好友只计一次。"),
2636
+ h("li", null, "每累计 ", h("b", null, n + " 位"), "有效邀请,自动发放 ", h("b", null, days + " 天 PRO 会员"), ",在原到期时间上顺延(已是长期会员则保持长期)。"),
2637
+ h("li", null, "奖励自动到账,无需申请:可在「我的邀请记录」下方看到「已到账奖励」。"),
2638
+ h("li", null, "邀请码与邀请链接长期有效,随时可在本页复制;好友解绑或重装设备不会撤回已计入的邀请。"),
2639
+ h("li", null, "规则与数值以本页面显示的为准(运营可能调整活动力度)。")
2640
+ ])
2641
+ : null
2642
+ );
2643
+ }
2644
+
2138
2645
  function renderInvite() {
2139
- var rule = (pub && pub.invite_rule) || { n: 3, days: 15 };
2646
+ var rule = (pub && pub.invite_rule) || {};
2647
+ var ruleN = Number(rule.n) > 0 ? Number(rule.n) : 3;
2648
+ var ruleDays = Number(rule.days) > 0 ? Number(rule.days) : 15;
2649
+ var trialDays = Number(pub && pub.trial_days) > 0 ? Number(pub.trial_days) : 0; // 注册送礼(关闭时不下发/为 0)
2140
2650
  var code = (account && account.invite_code) || "";
2141
2651
  var base = (st && st.remoteUrl) || (pub && pub.app_url) || "https://n.risegao.cn:13443/app/";
2142
- var link = base.replace(/\/+$/, "") + "/?invite=" + encodeURIComponent(code);
2143
- var copyInvite = function () {
2144
- try { navigator.clipboard.writeText(link).then(function () { setCopied(true); setTimeout(function () { setCopied(false); }, 1500); }); } catch (e) {}
2652
+ var link = code ? base.replace(/\/+$/, "") + "/?invite=" + encodeURIComponent(code) : "";
2653
+ var loggedInSaaS = !!(st && st.config && st.config.phone);
2654
+ var records = (inviteData && inviteData.records) || [];
2655
+ var rewards = (inviteData && inviteData.rewards) || [];
2656
+ var validCount = records.length;
2657
+ // 进度:每 ruleN 位结算一次(与后端 onDeviceBound 的 floor(total/n) 语义一致)
2658
+ var toward = ruleN > 0 ? validCount % ruleN : 0;
2659
+ var nextGap = toward === 0 ? ruleN : ruleN - toward;
2660
+ var pct = ruleN > 0 ? Math.round((toward / ruleN) * 100) : 0;
2661
+ var copyLink = function () {
2662
+ copyText(link).then(function (done) {
2663
+ if (!done) { setInviteCopied(""); setInviteCopyErr("复制失败:请长按/手动选中上面的链接复制"); return; }
2664
+ setInviteCopyErr(null);
2665
+ setInviteCopied("link");
2666
+ later(function () { setInviteCopied(""); }, 2000);
2667
+ });
2145
2668
  };
2669
+
2146
2670
  return h("div", null,
2147
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { width: "100%" }, onClick: function () { setView("home"); } }, "← 返回"),
2148
- card("🎯 邀请好友赚会员", [
2149
- h("div", { className: "dru-hint", style: { marginBottom: 8 } },
2150
- "每邀请 " + rule.n + " 位好友安装并注册,你获得 " + rule.days + " 天 PRO 会员(好友完成设备安装后计入有效邀请)。"),
2151
- code ? h("div", null,
2152
- h("div", { className: "dru-hint" }, "我的邀请码: " + code),
2153
- h("div", { className: "dru-url" },
2154
- h("span", null, link),
2155
- h("button", { type: "button", onClick: copyInvite }, copied ? "已复制" : "复制链接")
2156
- ),
2157
- h("div", { className: "dru-hint", style: { marginTop: 8 } },
2158
- "把链接发给好友,或把邀请码「" + code + "」告诉他们,注册时填写即可。")
2159
- ) : h("div", null,
2160
- h("div", { className: "dru-hint" }, "需要手机号账号登录后才能生成专属邀请链接。"),
2161
- h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%", marginTop: 10 }, disabled: busy !== "", onClick: function () {
2162
- setBusy("invite-refresh");
2163
- api("/dsh-remote/account").then(function (b) {
2164
- if (b && b.ok && b.account && b.account.invite_code) { setAccount(b.account); setMsg("ok", "邀请码已生成"); }
2165
- else setMsg("err", "获取邀请码失败,请稍后重试或先登录手机号账号");
2166
- }).catch(function (e) { setMsg("err", "获取邀请码失败:" + e.message); })
2167
- .finally(function () { setBusy(""); });
2168
- } }, busy === "invite-refresh" ? "生成中…" : "获取邀请码")
2169
- ),
2170
- h("div", { className: "dru-hint", style: { marginTop: 10 } }, "有效邀请记录(登录可见):")
2671
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { width: "100%" }, onClick: function () { setView("home"); } }, "← 返回账号"),
2672
+
2673
+ // ① 奖励是什么(大字 + 一句人话)
2674
+ card("🎯 邀请好友,一起用远程访问", [
2675
+ h("div", { className: "dru-invite-hero" },
2676
+ "邀请 ", h("em", null, ruleN + " 位"), "好友 → 你得 ", h("em", null, ruleDays + " 天 PRO 会员")),
2677
+ h("div", { className: "dru-invite-sub" },
2678
+ "好友用你的邀请链接注册,并在这台电脑上装好后台服务后计入;每满 " + ruleN + " 位自动到账,会员天数可累加"
2679
+ + (trialDays > 0 ? "。好友自己注册即得 " + trialDays + " 天 PRO 试用,不用邀请也一样。" : "。")),
2680
+ // 进度:已邀请 / 已到账 / 还差几位(数据全部来自接口,未登录或未加载时不编造数字)
2681
+ loggedInSaaS
2682
+ ? h("div", { className: "dru-invite-prog" },
2683
+ h("div", { className: "dru-invite-prog-top" },
2684
+ h("span", null, "已邀请 ", h("span", { className: "n" }, String(validCount)), " 位好友"),
2685
+ inviteErr ? null : h("span", null, "已到账 " + rewards.length + " 次(每次 " + ruleDays + " 天)")
2686
+ ),
2687
+ h("div", { className: "dru-invite-bar", role: "img", "aria-label": "距离下一次奖励的进度" },
2688
+ h("div", { className: "dru-invite-bar-fill", style: { width: pct + "%" } })),
2689
+ h("div", { className: "dru-invite-next" },
2690
+ inviteLoading
2691
+ ? "正在读取你的邀请记录…"
2692
+ : "本轮已累计 " + toward + "/" + ruleN + " 位,再邀请 " + nextGap + " 位即可获得下一次 " + ruleDays + " 天 PRO 会员。")
2693
+ )
2694
+ : null,
2695
+ // ② 我的邀请码 / 邀请链接(一键复制)
2696
+ code
2697
+ ? h("div", null,
2698
+ copyRow("我的邀请码", code, "code", "好友注册时填这个码,或直接用下面的邀请链接(自动带上)。"),
2699
+ copyRow("我的邀请链接", link, "link", "链接较长,可一键复制后发给好友;好友打开即进入注册页。")
2700
+ )
2701
+ : loggedInSaaS
2702
+ ? h("div", { style: { marginTop: 10 } },
2703
+ emptyState("🎫", "正在为你生成专属邀请码",
2704
+ "邀请码绑定你的手机号账号:好友通过它注册,奖励会自动记到这个账号上。(偶尔需要手动获取一次)",
2705
+ [h("button", {
2706
+ type: "button",
2707
+ className: "dru-btn dru-btn-primary",
2708
+ disabled: busy !== "",
2709
+ onClick: function () {
2710
+ setBusy("invite-refresh");
2711
+ api("/dsh-remote/account").then(function (b) {
2712
+ if (b && b.ok && b.account && b.account.invite_code) { setAccount(b.account); setMsg("ok", "邀请码已生成,可一键复制"); }
2713
+ else setMsg("err", "获取邀请码失败:请稍后重试(若仍未登录,请先在「🔑 账号」登录)");
2714
+ }).catch(function (e) { setMsg("err", "获取邀请码失败:" + e.message); })
2715
+ .finally(function () { setBusy(""); });
2716
+ }
2717
+ }, busy === "invite-refresh" ? "生成中…" : "获取我的邀请码")])
2718
+ )
2719
+ : h("div", { style: { marginTop: 10 } },
2720
+ emptyState("🔑", "登录后即可获得你的专属邀请码",
2721
+ "邀请码绑定你的手机号账号:好友通过它注册,奖励会自动记到这个账号上。登录后回到本页即可复制分享。",
2722
+ [h("button", { type: "button", className: "dru-btn dru-btn-primary", onClick: function () { setView("home"); } }, "去登录")])
2723
+ )
2171
2724
  ]),
2172
- code ? card("我的邀请记录",
2173
- (inviteData && inviteData.records && inviteData.records.length)
2174
- ? h("div", { className: "dru-status-line" }, "已邀请 " + inviteData.records.length + " 人(有效绑定), 获得奖励 " + ((inviteData.rewards || []).length) + " 次")
2175
- : h("div", { className: "dru-hint" }, "暂无有效邀请记录。邀请好友安装并注册,完成设备绑定后即计入。")
2176
- ) : null
2725
+
2726
+ // ③ 邀请记录(谁 / 什么时候 / 是否已生效)
2727
+ loggedInSaaS
2728
+ ? card("我的邀请记录" + (inviteErr ? "" : "(" + validCount + ")"), [
2729
+ inviteLoading
2730
+ ? h("div", { className: "dru-invite-sub" }, "正在读取邀请记录…")
2731
+ : inviteErr
2732
+ ? h("div", null,
2733
+ h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 0 } }, "读取邀请记录失败:" + inviteErr),
2734
+ h("div", { className: "dru-actions", style: { marginTop: 8 } },
2735
+ h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: inviteLoading, onClick: loadInvite }, "重试")))
2736
+ : validCount === 0
2737
+ ? emptyState("📨", "还没有好友通过邀请安装",
2738
+ "把上面的邀请链接发给好友:好友注册并在电脑上装好后台服务后,会自动出现在这里。")
2739
+ : h("div", null, [
2740
+ h("div", { className: "dru-rec-head" },
2741
+ h("span", { style: { flex: "1" } }, "好友"),
2742
+ h("span", null, "状态"),
2743
+ h("span", null, "时间")),
2744
+ renderInviteRecords(records, ruleN),
2745
+ renderInviteRewards(rewards, ruleDays),
2746
+ h("div", { className: "dru-invite-sub", style: { marginTop: 6 } },
2747
+ "名单里的好友都已完成电脑端安装与设备绑定,所以都算有效邀请。")
2748
+ ])
2749
+ ])
2750
+ : null,
2751
+
2752
+ // ④ 活动规则(默认折叠)
2753
+ renderInviteRules(ruleN, ruleDays),
2754
+
2755
+ // 复制失败等就地提示见各 copyRow(错误靠近出错位置)
2756
+ message ? h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text) : null
2177
2757
  );
2178
2758
  }
2179
2759
 
@@ -2202,6 +2782,18 @@ window.__ModuleLoader__.load({
2202
2782
  var dotOn = loggedInSaaS ? (connInfo ? connOnline : serviceRunning) : false;
2203
2783
  var dotCls = "dru-dot " + (dotOn ? "dru-dot-on" : "dru-dot-off");
2204
2784
 
2785
+ // 【0.6.9】「连接偏慢」判定(只用既有字段 + 本地计时,不新增/不假设后端字段):
2786
+ // ① 服务端下发 attempts ≥ 3(已经自动重试多次);② 阶段文案里出现限速/带宽/较慢类提示;
2787
+ // ③ 本地观察到「非 online 阶段持续 ≥ SLOW_CONNECT_HINT_MS」。
2788
+ // 连上(online)或未登录即清零;三者都不成立 → 不显示任何邀请入口(宁可不显示,也不误报)。
2789
+ if (!loggedInSaaS || connPhase === "online") slowConnTrack.since = 0;
2790
+ else if (!slowConnTrack.since) slowConnTrack.since = Date.now();
2791
+ var connSlowText = String(connText || "") + " " + String((connInfo && connInfo.detail) || "");
2792
+ var connSlow = loggedInSaaS && !!connInfo && connPhase !== "online" &&
2793
+ (Number(connInfo.attempts) >= 3 ||
2794
+ /限速|带宽|较慢|拥堵|拥塞/.test(connSlowText) ||
2795
+ (slowConnTrack.since > 0 && Date.now() - slowConnTrack.since >= SLOW_CONNECT_HINT_MS));
2796
+
2205
2797
  /**
2206
2798
  * 连接阶段区块:把「环境 → bridge 进程 → 中继注册」的自动推进过程如实展示出来,
2207
2799
  * 面向非技术用户——非 online 阶段一律说明「正在自动进行,无需操作」;
@@ -2229,16 +2821,25 @@ window.__ModuleLoader__.load({
2229
2821
  h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: retryConnect }, busy === "connect-retry" ? "重试中…" : "重试"),
2230
2822
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: copyDiagnostics }, copiedDiag ? "已复制" : "复制诊断信息")));
2231
2823
  rows.push(h("div", { className: "dru-hint", style: { marginTop: 6 } },
2232
- "查看日志:bridge " + (connInfo.logPath || "") + " ,安装 " + (connInfo.installLogPath || "") + "(把「复制诊断信息」的内容发给客服可加速定位)"));
2824
+ "查看日志:后台服务 " + (connInfo.logPath || "") + " ,安装 " + (connInfo.installLogPath || "") + "(把「复制诊断信息」的内容发给客服可加速定位)"));
2233
2825
  return h("div", null, rows);
2234
2826
  }
2235
2827
  // 非错误阶段:只解释「正在自动做什么」,并说明不需要任何操作
2236
2828
  if (connInfo.detail) rows.push(h("div", { className: "dru-hint", style: { marginTop: 6 } }, connInfo.detail));
2237
2829
  if (connInfo.installing || phase === "no_runtime") {
2238
2830
  rows.push(h("div", { className: "dru-hint", style: { marginTop: 4 } },
2239
- "首次安装会自动下载并配置,期间请不要关闭 DeepSeek;装完会自动启动 bridge,无需任何操作。"));
2831
+ "首次安装会自动下载并配置,期间请不要关闭 DeepSeek;装完会自动启动后台服务,无需任何操作。"));
2240
2832
  }
2241
2833
  if (connInfo.deviceId) rows.push(h("div", { className: "dru-meta" }, "设备 ID:" + connInfo.deviceId));
2834
+ // 【0.6.9】连接明显偏慢时的轻量邀请入口(不打断、不弹窗):
2835
+ // 判定只用既有字段 —— 服务端重试次数 attempts≥3、或阶段文案里出现限速/带宽/较慢类提示、
2836
+ // 或本地观察「非 online 阶段持续 ≥90s」。判断不了就不显示(宁可不出现,也不误报)。
2837
+ if (connSlow) {
2838
+ rows.push(h("div", { className: "dru-hint", style: { marginTop: 8 } },
2839
+ "连接比平时慢一些(仍在自动推进,无需操作):等待时也可以",
2840
+ renderInviteLink("🎯 邀请好友得会员", "连接偏慢时顺手邀请好友 —— 每满若干位得 PRO 会员奖励"),
2841
+ ",好友装好后双方访问都会更快。"));
2842
+ }
2242
2843
  rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
2243
2844
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: retryConnect }, busy === "connect-retry" ? "立即重试中…" : "立即重试"),
2244
2845
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: copyDiagnostics }, copiedDiag ? "已复制" : "复制诊断信息")));
@@ -2292,8 +2893,8 @@ window.__ModuleLoader__.load({
2292
2893
  h("div", { className: "dru-hint", style: { marginTop: 4 } },
2293
2894
  !serviceRunning
2294
2895
  ? (connInfo && connPhase !== "online"
2295
- ? "本机 bridge 正在自动准备中(" + connText + "):连上后这个链接/二维码即可使用,无需其他操作。"
2296
- : "本机 bridge 未运行:先在下方「🖥 Bridge 服务」卡启动。")
2896
+ ? "本机后台服务正在自动准备中(" + connText + "):连上后这个链接/二维码即可使用,无需其他操作。"
2897
+ : "本机后台服务未运行:先在下方「🖥 后台服务」卡启动。")
2297
2898
  : "打开链接/扫码进入即登录态;同设备重复扫码只更新授权,不新增设备。")
2298
2899
  )
2299
2900
  )
@@ -2482,13 +3083,15 @@ window.__ModuleLoader__.load({
2482
3083
  "服务器地址由你自行部署决定:填写你自建的 dsh-remote 服务地址与访问密钥即可(不是上面的云端地址)。部署方法见 README「自建部署」一节;切回云端随时可登录恢复。")
2483
3084
  )
2484
3085
  ),
2485
- // Bridge 状态
2486
- card("🖥 Bridge 服务",
3086
+ // 后台服务(原「Bridge 服务」:面向非技术用户统一叫「后台服务」,卡内配一句人话解释)
3087
+ card("🖥 后台服务",
2487
3088
  h("div", { className: "dru-status-line" },
2488
3089
  h("span", { className: "dru-dot " + (serviceRunning ? "dru-dot-on" : "dru-dot-off") }),
2489
3090
  h("span", null, st ? serviceStateText : "查询中…"),
2490
3091
  launchdPid ? h("span", { className: "dru-meta", style: { marginTop: 0 } }, "(pid=" + launchdPid + ")") : null
2491
3092
  ),
3093
+ h("div", { className: "dru-hint", style: { marginTop: 6 } },
3094
+ "「后台服务」=装在这台电脑上、负责把手机和电脑连起来的小程序(内部名 bridge)。面板会自动安装、自动启动,一般不需要你手动操作;只有状态一直停在「已停止」时才需要点下面的按钮。"),
2492
3095
  // 自动连接进度(登录后由面板 2.5s 短轮询自动推进):让用户在 bridge 卡也能一眼看到
2493
3096
  // 「到底走到哪一步了」——进程在跑≠能用,注册到中继才算。
2494
3097
  connInfo && connPhase && connPhase !== "no_account"
@@ -2496,21 +3099,21 @@ window.__ModuleLoader__.load({
2496
3099
  : null,
2497
3100
  h("div", { className: "dru-actions", style: { marginTop: 10 } },
2498
3101
  !serviceRunning
2499
- ? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: function () { toggleBridge(true); } }, busy === "start" ? "启动中…" : "启动 bridge")
2500
- : h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { toggleBridge(false); } }, busy === "stop" ? "停止中…" : "停止 bridge")
3102
+ ? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: function () { toggleBridge(true); } }, busy === "start" ? "启动中…" : "启动后台服务")
3103
+ : h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { toggleBridge(false); } }, busy === "stop" ? "停止中…" : "停止后台服务")
2501
3104
  ),
2502
3105
  h("div", { className: "dru-meta" }, st && st.config && st.config.deviceId ? "设备 ID:" + st.config.deviceId : "设备 ID:生成中"),
2503
3106
  h("div", { className: "dru-meta" }, st ? (st.service && (st.service.plistExists || st.service.serviceManager === "detached")
2504
3107
  ? (st.service.serviceManager === "detached"
2505
3108
  ? (st.service.autostartTask
2506
3109
  ? "自启动:已注册登录任务(任务计划程序 dsh-remote-bridge)"
2507
- : "自启动:未注册登录任务(由面板在 dsh web 启动时自动拉起 bridge)")
3110
+ : "自启动:未注册登录任务(由面板在 dsh web 启动时自动拉起后台服务)")
2508
3111
  : "自启动服务已安装")
2509
3112
  : "自启动服务未安装(启动时自动创建)") : ""),
2510
3113
  // 0.6.2:把「运行环境缺失 / launchd 崩溃循环」如实告诉用户,而不是显示「运行中」
2511
3114
  st && !serviceRuntimeReady
2512
3115
  ? h("div", { className: "dru-hint", style: { marginTop: 8 } },
2513
- "🛠 插件市场只装「面板插件」,桌面运行环境(bridge)需要额外补装——已自动在后台安装,装完会自动启动 bridge,不用做任何操作。若超过几分钟仍未变成「运行中」,点上方版本卡的「一键更新」手动补全。")
3116
+ "🛠 在插件市场搜索「dsh-remote-web」装上的只是「面板插件」,这台电脑上的后台服务需要额外补装——已自动在后台安装,装完会自动启动,不用做任何操作。若超过几分钟仍未变成「运行中」,点上方版本卡的「一键更新」手动补全即可。")
2514
3117
  : serviceCrashing
2515
3118
  ? h("div", { className: "dru-hint", style: { marginTop: 8 } },
2516
3119
  "⚠️ 上一次启动失败(自启动入口失效),已自动清理失效自启动项并重新补装运行环境。稍候会自动恢复;仍失败请点「一键更新」。")
@@ -2520,13 +3123,15 @@ window.__ModuleLoader__.load({
2520
3123
  "⚠️ 设备注册失败:" + (st.service.bindError.message || "未说明原因"),
2521
3124
  h("div", { className: "dru-hint", style: { marginTop: 4 } },
2522
3125
  st.service.bindError.code === "device_limit_exceeded"
2523
- ? "已达本套餐设备数上限。若是同一台电脑重装,稍等片刻会自动顶替旧设备;仍未恢复请在手机端「设备管理」解绑旧设备(免费用户每月可解绑 3 次)后,回到这里点「启动 bridge」。"
3126
+ ? "已达本套餐设备数上限。若是同一台电脑重装,稍等片刻会自动顶替旧设备;仍未恢复请在手机端「设备管理」解绑旧设备(免费用户每月可解绑 3 次)后,回到这里点「启动后台服务」。"
2524
3127
  : "请确认网络与账号状态后重试;仍未解决可点下方「彻底卸载」后重新安装。")
2525
3128
  )
2526
3129
  : null
2527
3130
  ),
2528
3131
  // 关于 dsh-remote(v0.5+ 远程访问价值说明卡片)
2529
3132
  card("📖 关于 dsh-remote", [
3133
+ // 一句话定位(面向非技术用户先讲清楚"这是什么"),下面 4 条为既有技术说明(文案受源码契约锁定)
3134
+ h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "📱 一句话:用手机随时接管这台电脑上的 DeepSeek Harness——装上即得专属加密地址,人在外面也能全功能操作(免内网穿透、免公网 IP、全程加密)。"),
2530
3135
  h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "📱 远程访问:用手机或另一台电脑的浏览器,随时随地使用同一份 dsh web——人在哪都能用(免公网 IP、免内网穿透);官方托管中继,4G/5G 即用,也可自建服务。"),
2531
3136
  h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。"),
2532
3137
  h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表。"),
@@ -2535,7 +3140,7 @@ window.__ModuleLoader__.load({
2535
3140
  // 只保留结论式的说明句:不再单列「隐私说明: README… · docs/telemetry.md」那一行文档链接
2536
3141
  // (面板里堆文档链接既占地方又不像产品文案;完整字段清单在仓库 docs/telemetry.md)。
2537
3142
  h("div", { className: "dru-hint", style: { marginTop: 6 } },
2538
- "📊 匿名装机统计:只上报「装机/连接是否成功」这类事件(安装开始与失败原因、bridge 是否注册成功、是否首次远程打通)——"
3143
+ "📊 匿名装机统计:只上报「装机/连接是否成功」这类事件(安装开始与失败原因、后台服务是否注册成功、是否首次远程打通)——"
2539
3144
  + "不含任何账号、手机号、会话或文件内容、主机名、路径、设备指纹与 IP;标识是本机随机 ID(重装即变)。"
2540
3145
  + "用环境变量 DSH_REMOTE_TELEMETRY=0 可完全关闭(不生成 ID、不发任何请求)。"),
2541
3146
  // 加入交流群(后台上传二维码后出现;点击弹出二维码大图便于扫码)
@@ -2610,7 +3215,8 @@ window.__ModuleLoader__.load({
2610
3215
  h("span", { className: "dru-settings-icon" }, "📱"),
2611
3216
  h("div", null,
2612
3217
  h("h2", { className: "dru-settings-title" }, "远程访问"),
2613
- h("div", { className: "dru-settings-sub" }, "通过手机或另一台电脑远程使用同一份 dsh web,人在哪都能用(免公网 IP)")
3218
+ h("div", { className: "dru-settings-sub" }, "通过手机或另一台电脑远程使用同一份 dsh web,人在哪都能用(免公网 IP)"),
3219
+ h("div", { className: "dru-settings-sub" }, "装上即得专属加密地址:人在外面也能用手机全功能接管这台电脑上的 DeepSeek Harness(免内网穿透、免公网 IP、全程加密)。")
2614
3220
  )
2615
3221
  ),
2616
3222
  h("div", { className: "dru-settings-body" },
@@ -2667,7 +3273,7 @@ window.__ModuleLoader__.load({
2667
3273
  ),
2668
3274
  // 常驻入口最底部:「重启 DeepSeek harness」按钮始终可达(首次安装/更新/排查都用它)
2669
3275
  h("div", { className: "dru-restart-foot" },
2670
- h("div", { className: "dru-hint" }, "重启 dsh web:仅在「刷新页面后仍看不到面板」时才需要。日常安装/更新插件已自动热加载,装完刷新页面即可;重启只影响 dsh web 本身,不影响 bridge 与手机端连接。"),
3276
+ h("div", { className: "dru-hint" }, "重启 dsh web:仅在「刷新页面后仍看不到面板」时才需要。日常安装/更新插件已自动热加载,装完刷新页面即可;重启只影响 dsh web 本身,不影响后台服务与手机端连接。"),
2671
3277
  h("button", {
2672
3278
  type: "button",
2673
3279
  className: "dru-btn " + (restartPending ? "dru-btn-primary" : "dru-btn-ghost"),