@mrrisega/dsh-remote 0.6.8 → 0.6.9-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/clients/dsh-remote/mobile-adapter.mjs +274 -21
- package/clients/dsh-remote/test/mobile-adapter-guards.test.mjs +682 -0
- package/docs/telemetry.md +5 -1
- package/dsh-setup.mjs +18 -18
- package/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +1496 -232
- package/packages/dsh-remote-web/lib/index.js +329 -29
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/test/update-stall.test.mjs +315 -0
|
@@ -41,143 +41,508 @@ window.__ModuleLoader__.load({
|
|
|
41
41
|
var styleEl = document.createElement("style");
|
|
42
42
|
styleEl.setAttribute("data-plugin", "dsh-remote-web");
|
|
43
43
|
styleEl.textContent = [
|
|
44
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
45
|
+
// 🎨 主题令牌层(--dru-*)—— 亮色 / 深色双套,**只作用于面板自己的根节点**
|
|
46
|
+
// ───────────────────────────────────────────────────────────────────────
|
|
47
|
+
// 宿主(DeepSeek Harness Web)表达「深色」的实测信号(2026-09 在本机 GUI 实读,见文末说明):
|
|
48
|
+
// ① document.body 上的属性 `data-ds-dark-theme` —— **存在即深色**(值恒为空串;
|
|
49
|
+
// 宿主写的是 body.toggleAttribute('data-ds-dark-theme', dark),不是 data-theme="dark")
|
|
50
|
+
// ② document.documentElement.style.colorScheme = 'dark' | 'light' —— **两种模式都会写**,
|
|
51
|
+
// 所以它同时能表达「浅色」,是判断「宿主是否已表态」的可靠依据
|
|
52
|
+
// 出处是宿主自己的实现:@deepseek-ai/dsh-client-ui-theme 的 boot 脚本 +
|
|
53
|
+
// @deepseek-ai/dsh-client-ui-layout 的 ThemePresenter(`html{color-scheme}` + body 调色板属性)。
|
|
54
|
+
//
|
|
55
|
+
// ⚠️ `prefers-color-scheme` **不是**宿主的表达方式:宿主只在用户选「跟随系统」时才看它
|
|
56
|
+
// (偏好存在 ~/.dsh/settings.yaml 的 ui-theme.preference,本机实测为 "light")。
|
|
57
|
+
// 所以本插件把它只当**兜底**(宿主没表态时才用系统偏好)。
|
|
58
|
+
//
|
|
59
|
+
// 令牌**只**定义在 .dru-settings-section / .dru-popup / .dru-nav-remote 上,
|
|
60
|
+
// 绝不写 :root / body —— 否则会污染整个 GUI。JS 解析出主题后往根节点写
|
|
61
|
+
// data-dru-theme="dark|light",下面按属性切换;末一条 `:not([data-dru-theme])`
|
|
62
|
+
// 是 JS 尚未执行时的纯 CSS 兜底(一旦 JS 写过属性就不再参与,不会打架)。
|
|
63
|
+
".dru-settings-section,.dru-popup,.dru-nav-remote{",
|
|
64
|
+
" color-scheme:light;",
|
|
65
|
+
" --dru-surface:#f6f8fa;", // 卡片底
|
|
66
|
+
" --dru-surface-2:#ffffff;", // 抬升面(输入框/行/弹窗卡)
|
|
67
|
+
" --dru-surface-3:#eaeef2;", // 再抬升 / 按下态 / 进度槽
|
|
68
|
+
" --dru-hover:#f6f8fa;", // 悬浮底
|
|
69
|
+
" --dru-fg:#1f2328;", // 正文
|
|
70
|
+
" --dru-fg-muted:#57606a;", // 次级文字
|
|
71
|
+
" --dru-border:#d0d7de;", // 面上描边(装饰)
|
|
72
|
+
" --dru-border-soft:#eaeef2;", // 更淡的分隔线
|
|
73
|
+
" --dru-border-ctl:#d0d7de;", // 控件描边(输入/按钮/页签)
|
|
74
|
+
" --dru-accent:#0969da;",
|
|
75
|
+
" --dru-accent-strong:#0860bd;",
|
|
76
|
+
" --dru-accent-active:#0757a8;",
|
|
77
|
+
" --dru-accent-fg:#0550ae;",
|
|
78
|
+
" --dru-accent-tint:#ddf4ff;",
|
|
79
|
+
" --dru-accent-tint-2:#f0f6ff;",
|
|
80
|
+
" --dru-accent-tint-border:#b6e3ff;",
|
|
81
|
+
" --dru-accent-fill:#0969da;", // 主按钮填充
|
|
82
|
+
" --dru-on-accent:#ffffff;", // 主按钮/头像上的文字
|
|
83
|
+
" --dru-tip-fg:#0a3069;", // 气泡正文
|
|
84
|
+
" --dru-tip-fg-strong:#0550ae;", // 气泡强调
|
|
85
|
+
" --dru-success:#1a7f37;",
|
|
86
|
+
" --dru-success-fg:#116329;",
|
|
87
|
+
" --dru-success-soft:#dafbe1;",
|
|
88
|
+
" --dru-success-border:#aceebb;",
|
|
89
|
+
" --dru-success-glow:rgba(26,127,55,.6);",
|
|
90
|
+
" --dru-warn:#9a6700;",
|
|
91
|
+
" --dru-warn-fg:#7d4e00;",
|
|
92
|
+
" --dru-warn-fg-2:#6b5900;",
|
|
93
|
+
" --dru-warn-soft:#fff8c5;",
|
|
94
|
+
" --dru-warn-border:#eed888;",
|
|
95
|
+
" --dru-warn-border-2:#d4a72c;",
|
|
96
|
+
" --dru-danger:#cf222e;",
|
|
97
|
+
" --dru-danger-soft:#fff0f1;",
|
|
98
|
+
" --dru-danger-soft-2:#ffdfe0;",
|
|
99
|
+
" --dru-danger-border:#ffb3b6;",
|
|
100
|
+
" --dru-dot-off:#6e7781;",
|
|
101
|
+
" --dru-reddot:#e5484d;",
|
|
102
|
+
" --dru-log-bg:#0d1117;",
|
|
103
|
+
" --dru-log-fg:#e6edf3;",
|
|
104
|
+
" --dru-overlay:rgba(0,0,0,.5);",
|
|
105
|
+
" --dru-shadow:rgba(0,0,0,.45);",
|
|
106
|
+
" --dru-focus:#0969da;",
|
|
107
|
+
" --dru-focus-halo:rgba(9,105,218,.28);",
|
|
108
|
+
" --dru-focus-halo-2:rgba(9,105,218,.18);",
|
|
109
|
+
" --dru-focus-halo-3:rgba(9,105,218,.15);",
|
|
110
|
+
"}",
|
|
111
|
+
// 深色:JS 写的 data-dru-theme 优先;宿主属性作 JS 未执行时的兜底。
|
|
112
|
+
// 取值跟着宿主的深色基色走(body 实测 #151517 / 层 1 #232324 / 层 2 #2c2c2e / 层 3 #353638),
|
|
113
|
+
// 语义色**在深底上重新取值**,不是沿用亮色值(亮色的 #1a7f37 / #cf222e 在深底对比度不足)。
|
|
114
|
+
".dru-settings-section[data-dru-theme=\"dark\"],.dru-popup[data-dru-theme=\"dark\"],.dru-nav-remote[data-dru-theme=\"dark\"],",
|
|
115
|
+
"body[data-ds-dark-theme] .dru-settings-section:not([data-dru-theme]),",
|
|
116
|
+
"body[data-ds-dark-theme] .dru-popup:not([data-dru-theme]),",
|
|
117
|
+
"body[data-ds-dark-theme] .dru-nav-remote:not([data-dru-theme]){",
|
|
118
|
+
" color-scheme:dark;",
|
|
119
|
+
" --dru-surface:#232324;",
|
|
120
|
+
" --dru-surface-2:#2c2c2e;",
|
|
121
|
+
" --dru-surface-3:#353638;",
|
|
122
|
+
" --dru-hover:#303236;",
|
|
123
|
+
" --dru-fg:#f0f2f5;",
|
|
124
|
+
" --dru-fg-muted:#b9bec6;",
|
|
125
|
+
" --dru-border:#52565c;",
|
|
126
|
+
" --dru-border-soft:#2f3237;",
|
|
127
|
+
" --dru-border-ctl:#6b7280;", // 对卡片 3.25:1 —— 控件轮廓在深底上认得出(WCAG 1.4.11)
|
|
128
|
+
" --dru-accent:#6ba4ff;",
|
|
129
|
+
" --dru-accent-strong:#8ab8ff;",
|
|
130
|
+
" --dru-accent-active:#a6c9ff;",
|
|
131
|
+
" --dru-accent-fg:#9cc4ff;",
|
|
132
|
+
" --dru-accent-tint:#16324d;",
|
|
133
|
+
" --dru-accent-tint-2:#1d3c5c;",
|
|
134
|
+
" --dru-accent-tint-border:#2a5580;",
|
|
135
|
+
" --dru-accent-fill:#2f6fd0;",
|
|
136
|
+
" --dru-on-accent:#ffffff;",
|
|
137
|
+
" --dru-tip-fg:#bcd8ff;",
|
|
138
|
+
" --dru-tip-fg-strong:#dbe9ff;",
|
|
139
|
+
" --dru-success:#56d364;",
|
|
140
|
+
" --dru-success-fg:#7ee787;",
|
|
141
|
+
" --dru-success-soft:#12351f;",
|
|
142
|
+
" --dru-success-border:#2b5c39;",
|
|
143
|
+
" --dru-success-glow:rgba(86,211,100,.45);",
|
|
144
|
+
" --dru-warn:#e3b341;",
|
|
145
|
+
" --dru-warn-fg:#f0c674;",
|
|
146
|
+
" --dru-warn-fg-2:#e8cd94;",
|
|
147
|
+
" --dru-warn-soft:#3a2d0c;",
|
|
148
|
+
" --dru-warn-border:#6b5417;",
|
|
149
|
+
" --dru-warn-border-2:#8a6d1f;",
|
|
150
|
+
" --dru-danger:#ff7b72;",
|
|
151
|
+
" --dru-danger-soft:#4a1d1f;",
|
|
152
|
+
" --dru-danger-soft-2:#5c2426;",
|
|
153
|
+
" --dru-danger-border:#7a383c;",
|
|
154
|
+
" --dru-dot-off:#8b9199;",
|
|
155
|
+
" --dru-reddot:#ff6b6b;",
|
|
156
|
+
" --dru-log-bg:#0b0d11;",
|
|
157
|
+
" --dru-log-fg:#e6edf3;",
|
|
158
|
+
" --dru-overlay:rgba(0,0,0,.66);",
|
|
159
|
+
" --dru-shadow:rgba(0,0,0,.66);",
|
|
160
|
+
" --dru-focus:#8ab8ff;",
|
|
161
|
+
" --dru-focus-halo:rgba(138,184,255,.30);",
|
|
162
|
+
" --dru-focus-halo-2:rgba(138,184,255,.22);",
|
|
163
|
+
" --dru-focus-halo-3:rgba(138,184,255,.20);",
|
|
164
|
+
"}",
|
|
44
165
|
// 设置页栏目容器(nav 选中后渲染在 settings.section 内容区)
|
|
45
|
-
".dru-settings-section{max-width:720px;display:flex;flex-direction:column;gap:14px;padding-top:2px}",
|
|
166
|
+
".dru-settings-section{max-width:720px;display:flex;flex-direction:column;gap:14px;padding-top:2px;color:var(--dru-fg)}",
|
|
46
167
|
".dru-settings-head{display:flex;align-items:center;gap:10px;padding:6px 2px 2px}",
|
|
47
168
|
".dru-settings-icon{font-size:24px;line-height:1;flex:none}",
|
|
48
|
-
|
|
49
|
-
".dru-settings-
|
|
169
|
+
// 标题/副标题优先沿用宿主语义令牌(自带深浅两套),取不到再退回本插件令牌
|
|
170
|
+
".dru-settings-title{margin:0;font-size:17px;font-weight:600;color:var(--dsw-alias-label-primary,var(--dru-fg))}",
|
|
171
|
+
".dru-settings-sub{font-size:12.5px;color:var(--dsw-alias-label-tertiary,var(--dru-fg-muted));margin-top:3px;line-height:1.6}",
|
|
50
172
|
".dru-settings-body{display:flex;flex-direction:column;gap:14px}",
|
|
51
173
|
// 首次安装引导小红点(挂在设置页「远程访问」导航栏目右上角)
|
|
52
|
-
".dru-reddot{position:absolute;top:9px;right:12px;width:7px;height:7px;border-radius:50%;background
|
|
53
|
-
".dru-card{background
|
|
54
|
-
".dru-card h3{margin:0 0 8px;font-size:13px;font-weight:700;color
|
|
55
|
-
".dru-url{background
|
|
56
|
-
".dru-url button{flex:none;border:1px solid
|
|
57
|
-
".dru-url button:hover{background
|
|
174
|
+
".dru-reddot{position:absolute;top:9px;right:12px;width:7px;height:7px;border-radius:50%;background:var(--dru-reddot);box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2,var(--dru-surface-2));pointer-events:none;z-index:1}",
|
|
175
|
+
".dru-card{background:var(--dru-surface);border:1px solid var(--dru-border-soft);border-radius:10px;padding:14px 16px;color:var(--dru-fg)}",
|
|
176
|
+
".dru-card h3{margin:0 0 8px;font-size:13px;font-weight:700;color:var(--dru-fg)}",
|
|
177
|
+
".dru-url{background:var(--dru-surface-2);border:1px solid var(--dru-border);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:var(--dru-fg)}",
|
|
178
|
+
".dru-url button{flex:none;min-height:44px;padding:10px 14px;border:1px solid var(--dru-border-ctl);background:var(--dru-surface-2);color:var(--dru-accent);border-radius:6px;font-size:12.5px;font-weight:600;cursor:pointer;font-family:inherit;transition:background .12s ease,border-color .12s ease}",
|
|
179
|
+
".dru-url button:hover{background:var(--dru-hover);border-color:var(--dru-accent)}",
|
|
180
|
+
".dru-url button:active{background:var(--dru-surface-3)}",
|
|
58
181
|
".dru-field{margin-bottom:11px}",
|
|
59
|
-
".dru-field > label{display:block;font-size:12.5px;font-weight:600;color
|
|
60
|
-
".dru-input{width:100%;box-sizing:border-box;padding:
|
|
61
|
-
".dru-input:focus{
|
|
182
|
+
".dru-field > label{display:block;font-size:12.5px;font-weight:600;color:var(--dru-fg);margin-bottom:5px}",
|
|
183
|
+
".dru-input{width:100%;box-sizing:border-box;min-height:44px;padding:10px 11px;border-radius:8px;border:1px solid var(--dru-border-ctl);background:var(--dru-surface-2);color:var(--dru-fg);font-size:13.5px;font-family:inherit}",
|
|
184
|
+
".dru-input:focus{border-color:var(--dru-accent);box-shadow:0 0 0 3px var(--dru-focus-halo)}",
|
|
62
185
|
".dru-actions{display:flex;gap:8px;flex-wrap:wrap}",
|
|
63
|
-
|
|
186
|
+
// 触控目标 ≥44px(相邻 8px,见 .dru-actions/.dru-tabs gap):按钮统一 min-height + hover/active 反馈
|
|
187
|
+
".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
188
|
".dru-btn:disabled{opacity:.55;cursor:default}",
|
|
65
|
-
".dru-btn-primary{background
|
|
66
|
-
".dru-btn-primary:hover:not(:disabled){background
|
|
67
|
-
".dru-btn-
|
|
68
|
-
".dru-btn-ghost:
|
|
69
|
-
".dru-btn-
|
|
70
|
-
".dru-btn-
|
|
71
|
-
".dru-
|
|
189
|
+
".dru-btn-primary{background:var(--dru-accent-fill);color:var(--dru-on-accent);border-color:var(--dru-accent-fill)}",
|
|
190
|
+
".dru-btn-primary:hover:not(:disabled){background:var(--dru-accent-strong);border-color:var(--dru-accent-strong)}",
|
|
191
|
+
".dru-btn-primary:active:not(:disabled){background:var(--dru-accent-active);border-color:var(--dru-accent-active)}",
|
|
192
|
+
".dru-btn-ghost{background:var(--dru-surface-2);color:var(--dru-fg);border-color:var(--dru-border-ctl)}",
|
|
193
|
+
".dru-btn-ghost:hover:not(:disabled){background:var(--dru-hover);border-color:var(--dru-accent)}",
|
|
194
|
+
".dru-btn-ghost:active:not(:disabled){background:var(--dru-surface-3)}",
|
|
195
|
+
".dru-btn-danger{background:var(--dru-surface-2);color:var(--dru-danger);border-color:var(--dru-danger)}",
|
|
196
|
+
".dru-btn-danger:hover:not(:disabled){background:var(--dru-danger-soft)}",
|
|
197
|
+
".dru-btn-danger:active:not(:disabled){background:var(--dru-danger-soft-2)}",
|
|
198
|
+
// 行内文字按钮:视觉高度不变(不破坏排版),用 ::after 把命中区扩到 ≥44px
|
|
199
|
+
".dru-linkbtn{display:inline-block;position:relative;padding:0;border:none;background:none;color:var(--dru-accent);font-size:12.5px;line-height:1.7;cursor:pointer;font-family:inherit;text-decoration:none}",
|
|
200
|
+
".dru-linkbtn::after{content:\"\";position:absolute;left:-8px;right:-8px;top:-12px;bottom:-12px}",
|
|
72
201
|
".dru-linkbtn:hover{text-decoration:underline}",
|
|
202
|
+
".dru-linkbtn:active{color:var(--dru-accent-fg)}",
|
|
73
203
|
".dru-tabs{display:flex;gap:8px;margin-bottom:12px}",
|
|
74
|
-
".dru-tab{flex:1;padding:
|
|
75
|
-
".dru-tab
|
|
204
|
+
".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:var(--dru-fg-muted);background:var(--dru-surface);border:1px solid var(--dru-border-ctl);user-select:none;transition:background .12s ease,border-color .12s ease}",
|
|
205
|
+
".dru-tab:hover{background:var(--dru-surface-2);border-color:var(--dru-accent);color:var(--dru-accent)}",
|
|
206
|
+
".dru-tab.active{color:var(--dru-accent);background:var(--dru-surface-2);border-color:var(--dru-accent)}",
|
|
76
207
|
".dru-captcha{display:flex;align-items:stretch;gap:8px}",
|
|
77
208
|
".dru-captcha .dru-input{flex:1;min-width:0}",
|
|
78
|
-
".dru-captcha-box{width:118px;height:
|
|
209
|
+
".dru-captcha-box{width:118px;height:44px;flex:none;border-radius:8px;border:1px solid var(--dru-border-ctl);cursor:pointer;background:var(--dru-surface);display:flex;align-items:center;justify-content:center;font-size:12px;color:var(--dru-fg-muted);overflow:hidden}",
|
|
79
210
|
".dru-captcha-box svg{display:block;width:100%;height:100%}",
|
|
80
211
|
".dru-user{display:flex;align-items:center;gap:10px;margin-bottom:12px}",
|
|
81
|
-
".dru-avatar{width:40px;height:40px;border-radius:50%;background
|
|
82
|
-
".dru-user .mail{font-size:14px;font-weight:600;color
|
|
83
|
-
".dru-user .plan{font-size:12px;color
|
|
84
|
-
".dru-status-line{display:flex;align-items:center;gap:8px;font-size:13px;color
|
|
212
|
+
".dru-avatar{width:40px;height:40px;border-radius:50%;background:var(--dru-accent-fill);color:var(--dru-on-accent);display:flex;align-items:center;justify-content:center;font-size:17px;font-weight:700;flex-shrink:0}",
|
|
213
|
+
".dru-user .mail{font-size:14px;font-weight:600;color:var(--dru-fg);word-break:break-all}",
|
|
214
|
+
".dru-user .plan{font-size:12px;color:var(--dru-fg-muted);margin-top:2px}",
|
|
215
|
+
".dru-status-line{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--dru-fg)}",
|
|
85
216
|
".dru-dot{width:8px;height:8px;border-radius:50%;display:inline-block;flex:none}",
|
|
86
|
-
".dru-dot-on{background
|
|
87
|
-
".dru-dot-off{background
|
|
88
|
-
".dru-meta{font-size:12px;color
|
|
217
|
+
".dru-dot-on{background:var(--dru-success);box-shadow:0 0 6px var(--dru-success-glow)}",
|
|
218
|
+
".dru-dot-off{background:var(--dru-dot-off)}",
|
|
219
|
+
".dru-meta{font-size:12px;color:var(--dru-fg-muted);margin-top:6px;word-break:break-all}",
|
|
89
220
|
".dru-msg{font-size:12.5px;min-height:18px;margin-top:8px}",
|
|
90
|
-
".dru-msg-ok{color
|
|
91
|
-
".dru-msg-err{color
|
|
92
|
-
".dru-msg-warn{color
|
|
93
|
-
".dru-hint{font-size:12px;color
|
|
221
|
+
".dru-msg-ok{color:var(--dru-success)}",
|
|
222
|
+
".dru-msg-err{color:var(--dru-danger)}",
|
|
223
|
+
".dru-msg-warn{color:var(--dru-warn)}",
|
|
224
|
+
".dru-hint{font-size:12px;color:var(--dru-fg-muted);line-height:1.6}",
|
|
94
225
|
// ── 重启 DeepSeek harness(首次安装/更新后置顶提醒 + 底部常驻按钮) ──
|
|
95
|
-
".dru-restart-alert{display:flex;gap:10px;align-items:flex-start;background
|
|
226
|
+
".dru-restart-alert{display:flex;gap:10px;align-items:flex-start;background:var(--dru-warn-soft);border:1px solid var(--dru-warn-border-2);border-radius:10px;padding:12px 14px;margin-bottom:12px}",
|
|
96
227
|
".dru-restart-alert-icon{font-size:18px;line-height:1.2}",
|
|
97
|
-
".dru-restart-alert-title{font-size:13px;font-weight:700;color
|
|
98
|
-
".dru-restart-alert-sub{font-size:12px;color
|
|
99
|
-
".dru-restart-foot{margin-top:14px;padding-top:12px;border-top:1px solid
|
|
228
|
+
".dru-restart-alert-title{font-size:13px;font-weight:700;color:var(--dru-warn-fg)}",
|
|
229
|
+
".dru-restart-alert-sub{font-size:12px;color:var(--dru-warn-fg-2);margin-top:4px;line-height:1.6}",
|
|
230
|
+
".dru-restart-foot{margin-top:14px;padding-top:12px;border-top:1px solid var(--dru-border-soft);display:flex;gap:10px;align-items:center;flex-wrap:wrap}",
|
|
100
231
|
".dru-restart-foot .dru-hint{flex:1;min-width:180px;margin:0}",
|
|
101
232
|
// ── 用户反馈模块 ──
|
|
102
233
|
".dru-fb-tabs{display:flex;gap:8px;margin-bottom:10px}",
|
|
103
|
-
".dru-fb-tab{flex:1;padding:
|
|
104
|
-
".dru-fb-tab.active{color
|
|
105
|
-
".dru-fb-select{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid
|
|
106
|
-
".dru-fb-textarea{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid
|
|
107
|
-
".dru-fb-textarea:focus{
|
|
108
|
-
".dru-fb-
|
|
234
|
+
".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:var(--dru-fg-muted);background:var(--dru-surface-3);border:1px solid var(--dru-border-ctl);user-select:none}",
|
|
235
|
+
".dru-fb-tab.active{color:var(--dru-accent);background:var(--dru-surface-2);border-color:var(--dru-accent)}",
|
|
236
|
+
".dru-fb-select{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid var(--dru-border-ctl);background:var(--dru-surface-2);color:var(--dru-fg);font-size:13.5px;font-family:inherit}",
|
|
237
|
+
".dru-fb-textarea{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid var(--dru-border-ctl);background:var(--dru-surface-2);color:var(--dru-fg);font-size:13.5px;font-family:inherit;resize:vertical;min-height:64px}",
|
|
238
|
+
".dru-fb-textarea:focus{border-color:var(--dru-accent);box-shadow:0 0 0 3px var(--dru-focus-halo)}",
|
|
239
|
+
".dru-fb-select:focus{border-color:var(--dru-accent);box-shadow:0 0 0 3px var(--dru-focus-halo)}",
|
|
240
|
+
".dru-fb-item{border:1px solid var(--dru-border-soft);border-radius:8px;background:var(--dru-surface-2);padding:10px 12px;margin-bottom:8px;color:var(--dru-fg)}",
|
|
109
241
|
".dru-fb-item-head{display:flex;align-items:center;gap:8px;margin-bottom:4px;flex-wrap:wrap}",
|
|
110
242
|
".dru-fb-badge{font-size:11px;border-radius:999px;padding:1px 8px;flex:none}",
|
|
111
|
-
".dru-fb-badge-open{color
|
|
112
|
-
".dru-fb-badge-processing{color
|
|
113
|
-
".dru-fb-badge-done{color
|
|
114
|
-
".dru-fb-cat{font-size:11px;border-radius:999px;padding:1px 8px;flex:none;color
|
|
115
|
-
".dru-fb-item-title{font-size:13px;font-weight:600;color
|
|
116
|
-
".dru-fb-item-content{font-size:12.5px;color
|
|
117
|
-
".dru-fb-item-time{font-size:
|
|
118
|
-
".dru-fb-reply{border-top:1px dashed
|
|
243
|
+
".dru-fb-badge-open{color:var(--dru-warn);background:var(--dru-warn-soft);border:1px solid var(--dru-warn-border)}",
|
|
244
|
+
".dru-fb-badge-processing{color:var(--dru-tip-fg-strong);background:var(--dru-accent-tint);border:1px solid var(--dru-accent-tint-border)}",
|
|
245
|
+
".dru-fb-badge-done{color:var(--dru-success-fg);background:var(--dru-success-soft);border:1px solid var(--dru-success-border)}",
|
|
246
|
+
".dru-fb-cat{font-size:11px;border-radius:999px;padding:1px 8px;flex:none;color:var(--dru-fg-muted);background:var(--dru-surface);border:1px solid var(--dru-border)}",
|
|
247
|
+
".dru-fb-item-title{font-size:13px;font-weight:600;color:var(--dru-fg);flex:1;min-width:120px}",
|
|
248
|
+
".dru-fb-item-content{font-size:12.5px;color:var(--dru-fg-muted);white-space:pre-wrap;word-break:break-word;margin:4px 0}",
|
|
249
|
+
".dru-fb-item-time{font-size:11.5px;color:var(--dru-fg-muted)}",
|
|
250
|
+
".dru-fb-reply{border-top:1px dashed var(--dru-border-soft);margin-top:8px;padding-top:8px}",
|
|
119
251
|
".dru-fb-reply-row{display:flex;gap:6px;align-items:flex-start;margin-bottom:6px}",
|
|
120
|
-
".dru-fb-reply-who{font-size:12px;font-weight:600;color
|
|
121
|
-
".dru-fb-reply-who.user{color
|
|
122
|
-
".dru-fb-reply-text{font-size:12.5px;color
|
|
123
|
-
".dru-fb-reply-input{width:100%;box-sizing:border-box;padding:7px 10px;border-radius:8px;border:1px solid
|
|
124
|
-
".dru-fb-empty{font-size:12.5px;color
|
|
252
|
+
".dru-fb-reply-who{font-size:12px;font-weight:600;color:var(--dru-accent);flex:none;width:76px}",
|
|
253
|
+
".dru-fb-reply-who.user{color:var(--dru-fg-muted)}",
|
|
254
|
+
".dru-fb-reply-text{font-size:12.5px;color:var(--dru-fg);white-space:pre-wrap;word-break:break-word;flex:1}",
|
|
255
|
+
".dru-fb-reply-input{width:100%;box-sizing:border-box;padding:7px 10px;border-radius:8px;border:1px solid var(--dru-border-ctl);background:var(--dru-surface-2);color:var(--dru-fg);font-size:12.5px;font-family:inherit;resize:vertical;min-height:44px}",
|
|
256
|
+
".dru-fb-empty{font-size:12.5px;color:var(--dru-fg-muted);text-align:center;padding:14px 0}",
|
|
125
257
|
// ── 满意度弹窗(1 小时体验后,只弹一次) ──
|
|
126
|
-
".dru-popup{position:fixed;inset:0;background:
|
|
127
|
-
".dru-popup-card{width:min(400px,calc(100vw - 48px));background
|
|
258
|
+
".dru-popup{position:fixed;inset:0;background:var(--dru-overlay);z-index:2147482000;display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}",
|
|
259
|
+
".dru-popup-card{width:min(400px,calc(100vw - 48px));background:var(--dru-surface-2);color:var(--dru-fg);border:1px solid var(--dru-border);border-radius:12px;box-shadow:0 24px 64px var(--dru-shadow);font-size:14px;line-height:1.5;font-family:var(--dsw-font-family,-apple-system,'PingFang SC','Microsoft YaHei',sans-serif);overflow:hidden}",
|
|
128
260
|
".dru-popup-body{padding:22px 22px 16px;text-align:center}",
|
|
129
261
|
".dru-popup-icon{font-size:34px;margin-bottom:8px}",
|
|
130
|
-
".dru-popup-title{font-size:16px;font-weight:700;color
|
|
131
|
-
".dru-popup-sub{font-size:12.5px;color
|
|
262
|
+
".dru-popup-title{font-size:16px;font-weight:700;color:var(--dru-fg);margin-bottom:6px}",
|
|
263
|
+
".dru-popup-sub{font-size:12.5px;color:var(--dru-fg-muted);margin-bottom:16px}",
|
|
132
264
|
".dru-popup-rate{display:flex;gap:10px;justify-content:center;margin-bottom:14px}",
|
|
133
|
-
".dru-popup-rate button{flex:1;max-width:96px;border:1px solid
|
|
134
|
-
".dru-popup-rate button:hover{border-color
|
|
135
|
-
".dru-popup-rate button.sel{border-color
|
|
136
|
-
".dru-popup-rate button .lbl{display:block;font-size:11px;color
|
|
137
|
-
".dru-popup-textarea{width:100%;box-sizing:border-box;padding:9px 11px;border-radius:8px;border:1px solid
|
|
138
|
-
".dru-popup-invite{background
|
|
265
|
+
".dru-popup-rate button{flex:1;max-width:96px;border:1px solid var(--dru-border-ctl);background:var(--dru-surface);border-radius:10px;padding:12px 6px;font-size:20px;cursor:pointer;font-family:inherit}",
|
|
266
|
+
".dru-popup-rate button:hover{border-color:var(--dru-accent);background:var(--dru-accent-tint)}",
|
|
267
|
+
".dru-popup-rate button.sel{border-color:var(--dru-accent);background:var(--dru-accent-tint);box-shadow:0 0 0 3px var(--dru-focus-halo-3)}",
|
|
268
|
+
".dru-popup-rate button .lbl{display:block;font-size:11px;color:var(--dru-fg-muted);margin-top:4px;font-weight:600}",
|
|
269
|
+
".dru-popup-textarea{width:100%;box-sizing:border-box;padding:9px 11px;border-radius:8px;border:1px solid var(--dru-border-ctl);background:var(--dru-surface-2);color:var(--dru-fg);font-size:13px;font-family:inherit;resize:vertical;min-height:56px;text-align:left}",
|
|
270
|
+
".dru-popup-invite{background:var(--dru-surface);border:1px dashed var(--dru-border);border-radius:8px;padding:12px;font-size:13px;color:var(--dru-fg-muted);text-align:center;margin-bottom:12px}",
|
|
139
271
|
".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:
|
|
141
|
-
".dru-popup-foot button{border:none;background:none;color
|
|
142
|
-
".dru-popup-foot button:hover{color
|
|
272
|
+
".dru-popup-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 16px;border-top:1px solid var(--dru-border-soft);font-size:12px;color:var(--dru-fg-muted)}",
|
|
273
|
+
".dru-popup-foot button{min-height:44px;border:none;background:none;color:var(--dru-fg-muted);cursor:pointer;font-size:12.5px;font-family:inherit;padding:10px 8px;border-radius:6px}",
|
|
274
|
+
".dru-popup-foot button:hover{color:var(--dru-accent);background:var(--dru-hover)}",
|
|
143
275
|
".dru-popup .dru-msg{text-align:left}",
|
|
144
|
-
".dru-community-qr{display:block;width:220px;max-width:62vw;margin:0 auto;background
|
|
145
|
-
".dru-restart-auto{display:flex;align-items:center;gap:8px;margin-top:10px;font-size:12.5px;color
|
|
146
|
-
".dru-restart-auto.muted{color
|
|
147
|
-
".dru-fb-community{margin-top:16px;padding-top:14px;border-top:1px dashed
|
|
276
|
+
".dru-community-qr{display:block;width:220px;max-width:62vw;margin:0 auto;background:var(--dru-surface-2);padding:10px;border-radius:10px;border:1px solid var(--dru-border);box-sizing:content-box}",
|
|
277
|
+
".dru-restart-auto{display:flex;align-items:center;gap:8px;margin-top:10px;font-size:12.5px;color:var(--dru-success);background:var(--dru-success-soft);border-radius:8px;padding:8px 10px}",
|
|
278
|
+
".dru-restart-auto.muted{color:var(--dru-fg-muted);background:var(--dru-surface)}",
|
|
279
|
+
".dru-fb-community{margin-top:16px;padding-top:14px;border-top:1px dashed var(--dru-border);text-align:center}",
|
|
148
280
|
".dru-fb-community .dru-community-qr{width:180px;max-width:56vw}",
|
|
149
281
|
// ── 自管理:版本与更新(插件面板内提供在线更新/彻底卸载,市场无更新按钮) ──
|
|
150
282
|
".dru-ver-badge{display:inline-block;font-size:11px;border-radius:999px;padding:1px 8px;margin-left:6px;vertical-align:1px}",
|
|
151
|
-
".dru-ver-badge-new{color
|
|
152
|
-
".dru-ver-badge-ok{color
|
|
153
|
-
".dru-up-log{margin-top:8px;background
|
|
283
|
+
".dru-ver-badge-new{color:var(--dru-warn);background:var(--dru-warn-soft);border:1px solid var(--dru-warn-border)}",
|
|
284
|
+
".dru-ver-badge-ok{color:var(--dru-success-fg);background:var(--dru-success-soft);border:1px solid var(--dru-success-border)}",
|
|
285
|
+
".dru-up-log{margin-top:8px;background:var(--dru-log-bg);color:var(--dru-log-fg);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}",
|
|
286
|
+
// 版本卡信息层级(0.6.9):版本号大字 + 通道 chip + 指标行(已用时/无输出)+ 卡住告警
|
|
287
|
+
".dru-ver-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
288
|
+
".dru-ver-num{font-size:15px;font-weight:700;color:var(--dru-fg)}",
|
|
289
|
+
".dru-ver-chip{font-size:11.5px;border-radius:999px;padding:2px 9px;color:var(--dru-tip-fg);background:var(--dru-accent-tint);border:1px solid var(--dru-accent-tint-border);white-space:nowrap}",
|
|
290
|
+
".dru-ver-metrics{margin-top:9px;background:var(--dru-surface-2);border:1px solid var(--dru-border);border-radius:8px;padding:9px 11px}",
|
|
291
|
+
".dru-ver-metric{display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12.5px;line-height:1.9;color:var(--dru-fg)}",
|
|
292
|
+
".dru-ver-metric > span:first-child{color:var(--dru-fg-muted)}",
|
|
293
|
+
".dru-stall{margin-top:9px;background:var(--dru-warn-soft);border:1px solid var(--dru-warn-border-2);border-radius:8px;padding:10px 12px;font-size:12.5px;line-height:1.65;color:var(--dru-warn-fg-2)}",
|
|
294
|
+
".dru-stall b{color:var(--dru-warn-fg)}",
|
|
295
|
+
".dru-spin{width:13px;height:13px;border-radius:50%;border:2px solid var(--dru-border);border-top-color:var(--dru-accent);display:inline-block;vertical-align:-2px;animation:dru-spin .9s linear infinite}",
|
|
296
|
+
"@keyframes dru-spin{to{transform:rotate(360deg)}}",
|
|
297
|
+
// ── 🎁 带新用户换会员(0.6.9):交换句式奖励大字 → 三步走 → 进度 → 邀请码/链接(一键复制)→ 折叠规则 → 记录 ──
|
|
298
|
+
".dru-invite-hero{font-size:20px;line-height:1.4;font-weight:700;color:var(--dru-fg);margin:2px 0 8px}",
|
|
299
|
+
".dru-invite-hero em{font-style:normal;color:var(--dru-accent-fg)}",
|
|
300
|
+
".dru-invite-sub{font-size:13px;line-height:1.65;color:var(--dru-fg-muted)}",
|
|
301
|
+
".dru-invite-sub b{color:var(--dru-fg)}",
|
|
302
|
+
".dru-invite-prog{margin-top:12px;background:var(--dru-surface-2);border:1px solid var(--dru-border);border-radius:8px;padding:10px 12px}",
|
|
303
|
+
".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:var(--dru-fg)}",
|
|
304
|
+
".dru-invite-prog-top .n{font-size:15px;font-weight:700;color:var(--dru-accent-fg)}",
|
|
305
|
+
".dru-invite-bar{height:8px;border-radius:999px;background:var(--dru-surface-3);overflow:hidden;margin:9px 0 7px}",
|
|
306
|
+
".dru-invite-bar-fill{height:100%;border-radius:999px;background:var(--dru-accent-fill);transition:width .3s ease}",
|
|
307
|
+
".dru-invite-next{font-size:12.5px;line-height:1.65;color:var(--dru-fg-muted)}",
|
|
308
|
+
".dru-copy-row{margin-top:12px}",
|
|
309
|
+
".dru-copy-row:first-child{margin-top:4px}",
|
|
310
|
+
".dru-copy-label{font-size:12.5px;font-weight:600;color:var(--dru-fg);margin-bottom:6px}",
|
|
311
|
+
".dru-copy-box{display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:var(--dru-surface-2);border:1px solid var(--dru-border);border-radius:8px;padding:9px 11px}",
|
|
312
|
+
".dru-code-val{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:19px;font-weight:700;letter-spacing:2.5px;color:var(--dru-fg);flex:1;min-width:140px;word-break:break-all}",
|
|
313
|
+
".dru-link-val{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;line-height:1.55;color:var(--dru-fg);flex:1;min-width:180px;word-break:break-all;overflow-wrap:anywhere}",
|
|
314
|
+
".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 var(--dru-accent-fill);background:var(--dru-accent-fill);color:var(--dru-on-accent);font:600 13px/1.2 inherit;cursor:pointer;transition:background .12s ease,border-color .12s ease}",
|
|
315
|
+
".dru-copy-btn:hover:not(:disabled){background:var(--dru-accent-strong);border-color:var(--dru-accent-strong)}",
|
|
316
|
+
".dru-copy-btn:active:not(:disabled){background:var(--dru-accent-active);border-color:var(--dru-accent-active)}",
|
|
317
|
+
".dru-copy-btn:disabled{opacity:.55;cursor:default}",
|
|
318
|
+
".dru-copy-btn.copied{background:var(--dru-success);border-color:var(--dru-success)}",
|
|
319
|
+
".dru-empty{text-align:center;padding:20px 14px;background:var(--dru-surface-2);border:1px dashed var(--dru-border);border-radius:10px}",
|
|
320
|
+
".dru-empty-icon{font-size:26px;line-height:1;margin-bottom:8px}",
|
|
321
|
+
".dru-empty-title{font-size:13.5px;font-weight:600;color:var(--dru-fg);margin-bottom:5px}",
|
|
322
|
+
".dru-empty-sub{font-size:12.5px;line-height:1.7;color:var(--dru-fg-muted);max-width:380px;margin:0 auto}",
|
|
323
|
+
".dru-empty-actions{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:14px}",
|
|
324
|
+
".dru-disclose{width:100%;box-sizing:border-box;min-height:44px;display:flex;align-items:center;justify-content:space-between;gap:10px;background:var(--dru-surface-2);border:1px solid var(--dru-border-ctl);border-radius:8px;padding:10px 12px;font:600 13px/1.3 inherit;color:var(--dru-fg);cursor:pointer;text-align:left}",
|
|
325
|
+
".dru-disclose:hover{background:var(--dru-hover);border-color:var(--dru-accent)}",
|
|
326
|
+
".dru-disclose-caret{font-size:12px;font-weight:600;color:var(--dru-fg-muted);flex:none}",
|
|
327
|
+
".dru-rules{margin:10px 0 0;padding-left:20px;font-size:12.5px;line-height:1.8;color:var(--dru-fg-muted)}",
|
|
328
|
+
".dru-rules li{margin-bottom:6px}",
|
|
329
|
+
".dru-rules b{color:var(--dru-fg)}",
|
|
330
|
+
".dru-rec{background:var(--dru-surface-2);border:1px solid var(--dru-border-soft);border-radius:8px;padding:10px 12px;margin-bottom:8px}",
|
|
331
|
+
".dru-rec-top{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
332
|
+
".dru-rec-who{font-size:13px;font-weight:600;color:var(--dru-fg);flex:1;min-width:120px;word-break:break-all}",
|
|
333
|
+
".dru-rec-when{font-size:12px;color:var(--dru-fg-muted)}",
|
|
334
|
+
".dru-rec-tag{font-size:11.5px;border-radius:999px;padding:1px 9px;flex:none;white-space:nowrap}",
|
|
335
|
+
".dru-rec-tag-ok{color:var(--dru-success-fg);background:var(--dru-success-soft);border:1px solid var(--dru-success-border)}",
|
|
336
|
+
".dru-rec-tag-wait{color:var(--dru-warn);background:var(--dru-warn-soft);border:1px solid var(--dru-warn-border)}",
|
|
337
|
+
".dru-rec-sub{font-size:12px;color:var(--dru-fg-muted);margin-top:5px;line-height:1.6}",
|
|
338
|
+
".dru-rec-head{display:flex;gap:8px;font-size:11.5px;font-weight:600;color:var(--dru-fg-muted);padding:0 4px 7px;border-bottom:1px solid var(--dru-border-soft);margin-bottom:9px}",
|
|
339
|
+
// ── 🔀 转化 or 拉新:痛点时刻的并列二选一(两条都是真按钮:命中区 ≥44px、可键盘聚焦) ──
|
|
340
|
+
// 蓝色左边框把它从周边灰字里提出来 —— 用户「正在疼」的时刻不再被一行 12px 灰字糊过去。
|
|
341
|
+
".dru-dual{margin-top:10px;background:var(--dru-surface-2);border:1px solid var(--dru-border);border-left:3px solid var(--dru-accent);border-radius:8px;padding:11px 12px}",
|
|
342
|
+
".dru-dual-title{font-size:13px;font-weight:700;color:var(--dru-fg);line-height:1.5}",
|
|
343
|
+
".dru-dual-sub{font-size:12.5px;line-height:1.65;color:var(--dru-fg-muted);margin-top:3px}",
|
|
344
|
+
// 窄屏(手机)自动上下堆叠;每个按钮都独占一行,绝不缩到点不中
|
|
345
|
+
".dru-dual-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}",
|
|
346
|
+
".dru-dual-actions .dru-btn{flex:1 1 210px}",
|
|
347
|
+
".dru-dual-note{font-size:12px;line-height:1.65;color:var(--dru-fg-muted);margin-top:8px}",
|
|
348
|
+
".dru-dual-note b{color:var(--dru-fg)}",
|
|
349
|
+
// 三步走(① 复制专属链接 → ② 发给还没注册过的新用户 → ③ 对方上线即到账)
|
|
350
|
+
".dru-steps{margin:10px 0 0;padding:0;list-style:none;counter-reset:dru-step}",
|
|
351
|
+
".dru-steps li{position:relative;padding-left:26px;font-size:12.5px;line-height:1.7;color:var(--dru-fg-muted);margin-bottom:7px}",
|
|
352
|
+
".dru-steps li:last-child{margin-bottom:0}",
|
|
353
|
+
".dru-steps li::before{counter-increment:dru-step;content:counter(dru-step);position:absolute;left:0;top:2px;width:18px;height:18px;border-radius:50%;background:var(--dru-accent-fill);color:var(--dru-on-accent);font-size:11px;font-weight:700;line-height:18px;text-align:center}",
|
|
354
|
+
".dru-steps b{color:var(--dru-fg)}",
|
|
355
|
+
// ── 🎁 邀请收益引导气泡(0.6.9):把「带 N 位新用户 → 得 M 天 PRO」摆到按钮旁 ──
|
|
356
|
+
// 数字来自 public-config 的 invite_rule(见 inviteRuleOf),关闭态/取不到时不渲染。
|
|
357
|
+
// 交互克制:进入时一次 320ms 弹入(power1.out),之后每 4.5s 一次轻微呼吸(占空比 ~28%,不是持续闪烁)。
|
|
358
|
+
".dru-tip-wrap{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:10px;max-width:100%;min-width:0}",
|
|
359
|
+
".dru-tip{position:relative;display:flex;align-items:center;gap:9px;flex:1 1 260px;min-width:0;box-sizing:border-box;background:var(--dru-accent-tint);border:1px solid var(--dru-accent-tint-border);border-radius:10px;padding:9px 42px 9px 11px;font-size:12.5px;line-height:1.6;color:var(--dru-tip-fg);animation:dru-tip-in .32s cubic-bezier(.215,.61,.355,1) both}",
|
|
360
|
+
".dru-tip b{color:var(--dru-tip-fg-strong);font-weight:700}",
|
|
361
|
+
".dru-tip-ic{flex:none;font-size:16px;line-height:1;display:inline-block;transform-origin:50% 50%;animation:dru-tip-breathe 4.5s ease-in-out 1.2s infinite}",
|
|
362
|
+
".dru-tip-close{position:absolute;top:4px;right:4px;width:30px;height:30px;display:flex;align-items:center;justify-content:center;padding:0;border:none;background:none;color:var(--dru-tip-fg);font-size:14px;line-height:1;cursor:pointer;border-radius:6px;font-family:inherit}",
|
|
363
|
+
// 视觉 30px,命中区补到 ≥44px(与 .dru-linkbtn 同一手法)
|
|
364
|
+
".dru-tip-close::after{content:\"\";position:absolute;left:-7px;right:-7px;top:-7px;bottom:-7px}",
|
|
365
|
+
".dru-tip-close:hover{background:var(--dru-accent-tint-2);color:var(--dru-tip-fg-strong)}",
|
|
366
|
+
"@keyframes dru-tip-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}",
|
|
367
|
+
"@keyframes dru-tip-breathe{0%,72%,100%{transform:scale(1)}82%{transform:scale(1.09)}}",
|
|
154
368
|
// ── 📱 远程访问(一次性访问密钥 + 已授权设备管理) ──
|
|
155
369
|
".dru-access-flex{display:flex;gap:14px;flex-wrap:wrap;align-items:flex-start;margin-top:10px}",
|
|
156
370
|
".dru-access-col{flex:1;min-width:230px;display:flex;flex-direction:column;gap:8px}",
|
|
157
|
-
".dru-qr-img{width:180px;height:180px;flex:none;border-radius:8px;border:1px solid
|
|
158
|
-
".dru-qr-ph{width:180px;height:180px;flex:none;border-radius:8px;border:1px dashed
|
|
371
|
+
".dru-qr-img{width:180px;height:180px;flex:none;border-radius:8px;border:1px solid var(--dru-border);background:var(--dru-surface-2);object-fit:contain}",
|
|
372
|
+
".dru-qr-ph{width:180px;height:180px;flex:none;border-radius:8px;border:1px dashed var(--dru-border);background:var(--dru-surface);color:var(--dru-fg-muted);font-size:12px;display:flex;align-items:center;justify-content:center;text-align:center;padding:10px;box-sizing:border-box}",
|
|
159
373
|
".dru-url.big{font-size:13.5px;font-weight:600}",
|
|
160
|
-
".dru-cd{font-size:12px;color
|
|
161
|
-
".dru-cd-ok{color
|
|
162
|
-
".dru-key-note{font-size:12px;color
|
|
163
|
-
".dru-dev{border:1px solid
|
|
374
|
+
".dru-cd{font-size:12px;color:var(--dru-warn);margin-top:2px}",
|
|
375
|
+
".dru-cd-ok{color:var(--dru-success)}",
|
|
376
|
+
".dru-key-note{font-size:12px;color:var(--dru-fg-muted);line-height:1.6}",
|
|
377
|
+
".dru-dev{border:1px solid var(--dru-border-soft);border-radius:8px;background:var(--dru-surface-2);padding:9px 11px;margin-bottom:8px}",
|
|
164
378
|
".dru-dev-top{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
165
|
-
".dru-dev-name{font-size:13px;font-weight:600;color
|
|
166
|
-
".dru-dev-meta{font-size:11.5px;color
|
|
167
|
-
".dru-dev-sub{font-size:
|
|
168
|
-
".dru-dev-tag{font-size:
|
|
169
|
-
".dru-dev-tag-off{color
|
|
379
|
+
".dru-dev-name{font-size:13px;font-weight:600;color:var(--dru-fg);flex:1;min-width:130px}",
|
|
380
|
+
".dru-dev-meta{font-size:11.5px;color:var(--dru-fg-muted)}",
|
|
381
|
+
".dru-dev-sub{font-size:11.5px;color:var(--dru-fg-muted);margin-top:4px}",
|
|
382
|
+
".dru-dev-tag{font-size:11.5px;color:var(--dru-fg-muted);border:1px solid var(--dru-border);border-radius:999px;padding:0 7px;flex:none;white-space:nowrap}",
|
|
383
|
+
".dru-dev-tag-off{color:var(--dru-danger);border-color:var(--dru-danger-border);background:var(--dru-danger-soft)}",
|
|
170
384
|
// Phase-5:端到端加密(E2EE)状态行 —— 启用=绿字绿点,未启用=灰字(纯文字状态行,不打扰)
|
|
171
|
-
".dru-e2ee-line{display:flex;align-items:center;gap:7px;margin-top:6px;font-size:12px;line-height:1.5;color
|
|
172
|
-
".dru-e2ee-line.ok{color
|
|
385
|
+
".dru-e2ee-line{display:flex;align-items:center;gap:7px;margin-top:6px;font-size:12px;line-height:1.5;color:var(--dru-fg-muted)}",
|
|
386
|
+
".dru-e2ee-line.ok{color:var(--dru-success)}",
|
|
173
387
|
// 侧栏「远程访问」快捷按钮(2026-09 恢复注入,与官方「设置」按钮共存不遮挡):
|
|
174
388
|
// 独立 button(非 clone),插在官方「设置」按钮之前,同源同布局不覆盖官方热区。
|
|
175
|
-
".dru-nav-remote{display:inline-flex;align-items:center;gap:6px;height:30px;margin:0 0 2px;padding:0 10px;border:1px solid
|
|
176
|
-
".dru-nav-remote:hover:not(:disabled){background
|
|
177
|
-
".dru-nav-remote-dot{position:absolute;top:-2px;right:-2px;width:8px;height:8px;border-radius:50%;background
|
|
389
|
+
".dru-nav-remote{display:inline-flex;align-items:center;gap:6px;height:30px;margin:0 0 2px;padding:0 10px;border:1px solid var(--dru-border-ctl);border-radius:8px;background:var(--dru-surface-2);color:var(--dru-accent);font:500 12.5px/1 inherit;cursor:pointer;white-space:nowrap;user-select:none}",
|
|
390
|
+
".dru-nav-remote:hover:not(:disabled){background:var(--dru-accent-tint-2);border-color:var(--dru-accent)}",
|
|
391
|
+
".dru-nav-remote-dot{position:absolute;top:-2px;right:-2px;width:8px;height:8px;border-radius:50%;background:var(--dru-reddot);box-shadow:0 0 0 2px var(--dru-surface-2);pointer-events:none}",
|
|
392
|
+
// 侧栏入口与官方按钮同高(30px);用 ::after 把命中区补到 ≥44px,不破坏与原按钮的对齐
|
|
393
|
+
".dru-nav-remote::after{content:\"\";position:absolute;left:0;right:0;top:-7px;bottom:-7px}",
|
|
394
|
+
// ── 无障碍基线(0.6.9)────────────────────────────────────────────────
|
|
395
|
+
// 键盘焦点必须可见:放在样式表末尾,覆盖上面任何 outline:none(同特异性后者胜)
|
|
396
|
+
".dru-settings-section :focus-visible,.dru-popup :focus-visible{outline:2px solid var(--dru-focus);outline-offset:2px}",
|
|
397
|
+
".dru-settings-section [role=\"region\"]:focus-visible{outline-offset:-2px}",
|
|
398
|
+
".dru-btn:focus-visible,.dru-copy-btn:focus-visible,.dru-tab:focus-visible,.dru-disclose:focus-visible{outline:2px solid var(--dru-focus);outline-offset:2px;box-shadow:0 0 0 4px var(--dru-focus-halo-2)}",
|
|
399
|
+
".dru-tip-close:focus-visible{outline:2px solid var(--dru-focus);outline-offset:1px}",
|
|
400
|
+
".dru-nav-remote:focus-visible{outline:2px solid var(--dru-focus);outline-offset:2px}",
|
|
401
|
+
// 尊重系统「减少动态效果」:关掉过渡与旋转动画(状态文字照常更新)。
|
|
402
|
+
// 🎁 引导气泡在这条下**完全不动**(静态显示),符合「reduce 时没有动效」的要求。
|
|
403
|
+
"@media (prefers-reduced-motion: reduce){",
|
|
404
|
+
" .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}",
|
|
405
|
+
" .dru-spin{animation:none !important;border-top-color:var(--dru-accent)}",
|
|
406
|
+
" .dru-tip{animation:none !important}",
|
|
407
|
+
" .dru-tip-ic{animation:none !important}",
|
|
408
|
+
"}",
|
|
409
|
+
// 窄屏(手机竖屏打开本机面板):卡片内边距收紧、代码/链接不溢出
|
|
410
|
+
"@media (max-width:520px){",
|
|
411
|
+
" .dru-card{padding:12px 12px}",
|
|
412
|
+
" .dru-invite-hero{font-size:18px}",
|
|
413
|
+
" .dru-copy-box{flex-direction:column;align-items:stretch}",
|
|
414
|
+
" .dru-copy-btn{width:100%}",
|
|
415
|
+
// 气泡独占一行,绝不把按钮挤到屏幕外(320px 下也不横向滚动)
|
|
416
|
+
" .dru-tip{flex:1 1 100%;padding:9px 40px 9px 10px;font-size:12px}",
|
|
417
|
+
"}",
|
|
178
418
|
].join("\n");
|
|
179
419
|
document.head.appendChild(styleEl);
|
|
180
420
|
|
|
421
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
422
|
+
// 🎨 主题跟随(深色 / 亮色)—— 解析宿主信号,实时跟随,不污染宿主
|
|
423
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
424
|
+
/**
|
|
425
|
+
* 宿主表达「深色」的**实测信号**(2026-09 在本机 GUI http://127.0.0.1:3080 用 headless
|
|
426
|
+
* Chromium + CDP 实读,不是猜的):
|
|
427
|
+
*
|
|
428
|
+
* ① `document.body` 上的属性 **`data-ds-dark-theme`** —— **存在即深色**,值恒为空串。
|
|
429
|
+
* 宿主写的是 `document.body.toggleAttribute('data-ds-dark-theme', dark)`,
|
|
430
|
+
* **不是** `data-theme="dark"`、也不是 `class="dark"`。
|
|
431
|
+
* ② `document.documentElement.style.colorScheme` = `'dark' | 'light'` —— **两种模式都会写**,
|
|
432
|
+
* 所以它同时能表达「浅色」,是判断「宿主是否已经表态」的可靠依据。
|
|
433
|
+
*
|
|
434
|
+
* 实测证据:在真实 GUI 里把 ① 翻成存在、并把 ② 写成 'dark' 之后(这正是宿主 ThemePresenter
|
|
435
|
+
* 的两行原文),`getComputedStyle(document.body).backgroundColor` 由 `rgb(255,255,255)`
|
|
436
|
+
* 变成 `rgb(21,21,23)`,`--dsw-alias-bg-base` 由 `#fff` 变成 `#151517`,
|
|
437
|
+
* `--dsw-alias-label-primary` 由 `#0f1115` 变成 `#f9fafb` —— 整套宿主题跟着切。还原后回到亮色。
|
|
438
|
+
*
|
|
439
|
+
* 出处(宿主自己的代码,非第三方猜测):
|
|
440
|
+
* · `@deepseek-ai/dsh-client-ui-theme/lib/index.js` 的 bootThemeScript():
|
|
441
|
+
* `document.documentElement.style.colorScheme = dark ? 'dark' : 'light'`
|
|
442
|
+
* `document.body.toggleAttribute('data-ds-dark-theme', dark)`
|
|
443
|
+
* · `@deepseek-ai/dsh-client-ui-layout` 的 ThemePresenter(运行期同两处 DOM 写入,
|
|
444
|
+
* `DARK_ATTRIBUTE = "data-ds-dark-theme"`)
|
|
445
|
+
*
|
|
446
|
+
* ⚠️ `prefers-color-scheme` **不是**宿主的表达方式 —— 宿主只在用户选「跟随系统」时才看它
|
|
447
|
+
* (偏好存 `~/.dsh/settings.yaml` 的 `ui-theme.preference`,本机实测 `light`;
|
|
448
|
+
* 实测该环境下 `matchMedia('(prefers-color-scheme: dark)').matches === false`,
|
|
449
|
+
* 而用户在 GUI 里切换主题**根本不会**改变它)。所以这里把它**只当兜底**:
|
|
450
|
+
* 宿主没表态(非 DSH 宿主、boot 脚本未执行)时才退回系统偏好。
|
|
451
|
+
*/
|
|
452
|
+
var DARK_ATTR = "data-ds-dark-theme";
|
|
453
|
+
var THEME_MQ = "(prefers-color-scheme: dark)";
|
|
454
|
+
/** 读宿主当前主题:'dark' | 'light'。任何异常都退回 'light'(绝不把面板打挂)。 */
|
|
455
|
+
function resolveTheme() {
|
|
456
|
+
try {
|
|
457
|
+
var body = document && document.body;
|
|
458
|
+
if (body && typeof body.hasAttribute === "function" && body.hasAttribute(DARK_ATTR)) return "dark";
|
|
459
|
+
var de = document && document.documentElement;
|
|
460
|
+
var cs = de && de.style ? String(de.style.colorScheme || "").trim().toLowerCase() : "";
|
|
461
|
+
if (cs === "dark") return "dark";
|
|
462
|
+
if (cs === "light") return "light";
|
|
463
|
+
// 宿主没表态 → 系统偏好兜底(`matchMedia` 在 node 测试沙箱里不存在,故双重探测)
|
|
464
|
+
var mq = typeof matchMedia === "function" ? matchMedia(THEME_MQ) : null;
|
|
465
|
+
if (!mq && typeof window !== "undefined" && window && typeof window.matchMedia === "function") mq = window.matchMedia(THEME_MQ);
|
|
466
|
+
if (mq && typeof mq.matches === "boolean") return mq.matches ? "dark" : "light";
|
|
467
|
+
} catch (e) { /* 忽略:判定失败一律按亮色 */ }
|
|
468
|
+
return "light";
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* 主题外部存储:面板根/弹窗用 useSyncExternalStore 订阅,侧栏按钮用 themeApplyToNav 直改。
|
|
472
|
+
* 用「存储 + 订阅」而不是 useState,是因为侧栏入口挂在面板之外、生命周期也更长。
|
|
473
|
+
*/
|
|
474
|
+
var themeState = { value: null, subs: [], obs: null, mq: null, watching: false, navEl: null };
|
|
475
|
+
function themeGet() {
|
|
476
|
+
if (themeState.value === null) themeState.value = resolveTheme();
|
|
477
|
+
return themeState.value;
|
|
478
|
+
}
|
|
479
|
+
function themeApplyToNav() {
|
|
480
|
+
try {
|
|
481
|
+
var el = themeState.navEl || document.getElementById(NAV_ENTRY_ID);
|
|
482
|
+
if (el && el.setAttribute) el.setAttribute("data-dru-theme", themeGet());
|
|
483
|
+
} catch (e) { /* 忽略 */ }
|
|
484
|
+
}
|
|
485
|
+
function themeSubscribe(fn) {
|
|
486
|
+
if (typeof fn !== "function") return function () {};
|
|
487
|
+
themeState.subs.push(fn);
|
|
488
|
+
themeStartWatch();
|
|
489
|
+
return function () {
|
|
490
|
+
var i = themeState.subs.indexOf(fn);
|
|
491
|
+
if (i >= 0) themeState.subs.splice(i, 1); // 组件卸载即退订,不泄漏
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/** 重算主题;变了才通知订阅者(避免无谓重渲染)。 */
|
|
495
|
+
function themeRecompute() {
|
|
496
|
+
var next = resolveTheme();
|
|
497
|
+
if (next === themeState.value) return;
|
|
498
|
+
themeState.value = next;
|
|
499
|
+
themeApplyToNav();
|
|
500
|
+
var subs = themeState.subs.slice();
|
|
501
|
+
for (var i = 0; i < subs.length; i++) { try { subs[i](); } catch (e) { /* 单个订阅者异常不影响其它 */ } }
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* 开始监听主题变化(幂等)。两个信号源:
|
|
505
|
+
* · MutationObserver:观察 `<body>` 的属性(data-ds-dark-theme 是属性)+ `<html>` 的
|
|
506
|
+
* style(color-scheme 写在 html 的内联 style 上)—— 宿主切换时两者都会变;
|
|
507
|
+
* · matchMedia 的 change 事件:兜底路径(跟随系统)时才会用上。
|
|
508
|
+
* 用户可能在面板开着的时候切主题,所以**必须**实时重算,不能只在加载时判一次。
|
|
509
|
+
*/
|
|
510
|
+
function themeStartWatch() {
|
|
511
|
+
if (themeState.watching) return;
|
|
512
|
+
themeState.watching = true;
|
|
513
|
+
try {
|
|
514
|
+
var obs = new MutationObserver(function () { themeRecompute(); });
|
|
515
|
+
if (document.body) obs.observe(document.body, { attributes: true, attributeFilter: [DARK_ATTR, "class", "style"] });
|
|
516
|
+
if (document.documentElement) obs.observe(document.documentElement, { attributes: true, attributeFilter: [DARK_ATTR, "class", "style"] });
|
|
517
|
+
themeState.obs = obs;
|
|
518
|
+
} catch (e) { /* 忽略:没有 MutationObserver 时仍靠 matchMedia 与首帧判定 */ }
|
|
519
|
+
try {
|
|
520
|
+
var mq = typeof matchMedia === "function" ? matchMedia(THEME_MQ) : null;
|
|
521
|
+
if (!mq && typeof window !== "undefined" && window && typeof window.matchMedia === "function") mq = window.matchMedia(THEME_MQ);
|
|
522
|
+
if (mq) {
|
|
523
|
+
if (typeof mq.addEventListener === "function") mq.addEventListener("change", themeRecompute);
|
|
524
|
+
else if (typeof mq.addListener === "function") mq.addListener(themeRecompute);
|
|
525
|
+
themeState.mq = mq;
|
|
526
|
+
}
|
|
527
|
+
} catch (e) { /* 忽略 */ }
|
|
528
|
+
try { if (window && window.addEventListener) window.addEventListener("pagehide", themeStopWatch); } catch (e) {}
|
|
529
|
+
themeRecompute();
|
|
530
|
+
}
|
|
531
|
+
/** 停止监听并释放资源(页面卸载时调用;订阅者仍在时下次 subscribe 会自动重启)。 */
|
|
532
|
+
function themeStopWatch() {
|
|
533
|
+
try { if (themeState.obs) themeState.obs.disconnect(); } catch (e) {}
|
|
534
|
+
themeState.obs = null;
|
|
535
|
+
try {
|
|
536
|
+
if (themeState.mq) {
|
|
537
|
+
if (typeof themeState.mq.removeEventListener === "function") themeState.mq.removeEventListener("change", themeRecompute);
|
|
538
|
+
else if (typeof themeState.mq.removeListener === "function") themeState.mq.removeListener(themeRecompute);
|
|
539
|
+
}
|
|
540
|
+
} catch (e) {}
|
|
541
|
+
themeState.mq = null;
|
|
542
|
+
try { if (window && window.removeEventListener) window.removeEventListener("pagehide", themeStopWatch); } catch (e) {}
|
|
543
|
+
themeState.watching = false;
|
|
544
|
+
}
|
|
545
|
+
|
|
181
546
|
// ── 首次安装引导小红点(设置页「远程访问」栏目,localStorage 控制) ────
|
|
182
547
|
// 无 dsh-remote-seen-dot key 视为首次:在设置页导航栏目右上角显示 CSS 圆点;
|
|
183
548
|
// 点击栏目/红点后写入 key(之后不再显示),重启 DSH Web 不复发。
|
|
@@ -199,6 +564,23 @@ window.__ModuleLoader__.load({
|
|
|
199
564
|
}
|
|
200
565
|
window.__dshRemoteDotWatch = false;
|
|
201
566
|
}
|
|
567
|
+
|
|
568
|
+
// ── 🎁 邀请收益引导气泡的「已关闭」记忆 ─────────────────────────────────
|
|
569
|
+
// 键名沿用本插件的统一前缀 dsh-remote-(与 dsh-remote-seen-dot 一致)。
|
|
570
|
+
// 用户关过一次就永久记住,不再每次进面板都骚扰;localStorage 不可用(隐私模式/被禁)
|
|
571
|
+
// 时 read 返回 false 但不会抛 —— 读不到就当作「没关过」,宁可多显示一次也不报错。
|
|
572
|
+
var INVITE_TIP_KEY = "dsh-remote-invite-tip-dismissed-v1";
|
|
573
|
+
// 气泡的 DOM id:触发按钮用 aria-describedby 指向它(同一屏只此一处)
|
|
574
|
+
var INVITE_TIP_ID = "dru-invite-tip";
|
|
575
|
+
// ⚠️ 命名注意:这两个函数名**不能**叫 inviteTipDismissed —— 组件里有一个同名的 `var`
|
|
576
|
+
// (useState 值)。`var` 会提升并遮蔽整个函数体,连 `useState(inviteTipWasDismissed())`
|
|
577
|
+
// 的参数都会被解析成那个尚未赋值的局部 var(undefined),导致「已关闭」记忆失效。
|
|
578
|
+
function inviteTipWasDismissed() {
|
|
579
|
+
try { return localStorage.getItem(INVITE_TIP_KEY) === "1"; } catch (e) { return false; }
|
|
580
|
+
}
|
|
581
|
+
function inviteTipDismiss() {
|
|
582
|
+
try { localStorage.setItem(INVITE_TIP_KEY, "1"); } catch (e) { /* 忽略 */ }
|
|
583
|
+
}
|
|
202
584
|
/** 给单个设置页导航栏目按钮挂红点(幂等)。 */
|
|
203
585
|
function dotInject(cell) {
|
|
204
586
|
try {
|
|
@@ -308,6 +690,12 @@ window.__ModuleLoader__.load({
|
|
|
308
690
|
btn.id = NAV_ENTRY_ID;
|
|
309
691
|
btn.className = "dru-nav-remote";
|
|
310
692
|
btn.setAttribute("data-dru-remote", "1");
|
|
693
|
+
// 🎨 侧栏入口在面板之外,拿不到面板根上的令牌 → 自己也挂一份主题属性
|
|
694
|
+
btn.setAttribute("data-dru-theme", themeGet());
|
|
695
|
+
themeState.navEl = btn;
|
|
696
|
+
// 入口常驻,所以它自己就是主题观察器的一个订阅者:
|
|
697
|
+
// 用户切深色时(哪怕面板没开着)这里也要立刻跟上。
|
|
698
|
+
themeStartWatch();
|
|
311
699
|
btn.title = "打开「远程访问」设置(快捷入口)";
|
|
312
700
|
btn.style.position = "relative";
|
|
313
701
|
btn.textContent = "📱 远程访问";
|
|
@@ -434,6 +822,62 @@ window.__ModuleLoader__.load({
|
|
|
434
822
|
try { clearTimeout(t); } catch (e) { /* 忽略 */ }
|
|
435
823
|
}
|
|
436
824
|
|
|
825
|
+
// ── 剪贴板(0.6.9 邀请区「一键复制」)────────────────────────────────────
|
|
826
|
+
/**
|
|
827
|
+
* 复制文本到剪贴板,返回 Promise<boolean>。
|
|
828
|
+
* 优先 Clipboard API;http 非安全上下文 / 旧浏览器里 navigator.clipboard 不存在
|
|
829
|
+
* → 退回隐藏 textarea + document.execCommand("copy");两种都失败返回 false,
|
|
830
|
+
* 调用方**必须**在按钮旁给出可见提示(不能让用户以为"点了没反应")。
|
|
831
|
+
*/
|
|
832
|
+
function copyText(text) {
|
|
833
|
+
var s = text == null ? "" : String(text);
|
|
834
|
+
if (!s) return Promise.resolve(false);
|
|
835
|
+
var fallback = function () { return Promise.resolve(copyTextLegacy(s)); };
|
|
836
|
+
try {
|
|
837
|
+
if (navigator && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
838
|
+
return navigator.clipboard.writeText(s).then(function () { return true; }).catch(fallback);
|
|
839
|
+
}
|
|
840
|
+
} catch (e) { /* 落入兜底 */ }
|
|
841
|
+
return fallback();
|
|
842
|
+
}
|
|
843
|
+
/** 兜底复制:隐藏 textarea + execCommand(同步;不可用/被拒绝时返回 false)。 */
|
|
844
|
+
function copyTextLegacy(s) {
|
|
845
|
+
var ta = null;
|
|
846
|
+
try {
|
|
847
|
+
ta = document.createElement("textarea");
|
|
848
|
+
ta.value = s;
|
|
849
|
+
ta.setAttribute("readonly", "readonly");
|
|
850
|
+
ta.style.position = "fixed";
|
|
851
|
+
ta.style.top = "-1000px";
|
|
852
|
+
ta.style.left = "-1000px";
|
|
853
|
+
ta.style.opacity = "0";
|
|
854
|
+
document.body.appendChild(ta);
|
|
855
|
+
try { ta.select(); } catch (e) { /* 某些环境无 select */ }
|
|
856
|
+
return !!(document.execCommand && document.execCommand("copy"));
|
|
857
|
+
} catch (e) {
|
|
858
|
+
return false;
|
|
859
|
+
} finally {
|
|
860
|
+
try { if (ta && ta.parentNode && ta.parentNode.removeChild) ta.parentNode.removeChild(ta); } catch (e) { /* 忽略 */ }
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
/** 毫秒 → 人话时长("12 秒" / "1 分 05 秒" / "1 小时 02 分");用于更新「已用时」。 */
|
|
864
|
+
function fmtDuration(ms) {
|
|
865
|
+
var n = Number(ms);
|
|
866
|
+
var s = Math.max(0, Math.round((isFinite(n) ? n : 0) / 1000));
|
|
867
|
+
if (s < 60) return s + " 秒";
|
|
868
|
+
var m = Math.floor(s / 60);
|
|
869
|
+
if (m < 60) return m + " 分 " + (s % 60 < 10 ? "0" : "") + (s % 60) + " 秒";
|
|
870
|
+
return Math.floor(m / 60) + " 小时 " + (m % 60 < 10 ? "0" : "") + (m % 60) + " 分";
|
|
871
|
+
}
|
|
872
|
+
/** 手机号掩码(邀请记录里的新用户手机号:面板只展示掩码,完整号码留在服务端)。 */
|
|
873
|
+
function maskPhoneLike(v) {
|
|
874
|
+
var s = String(v == null ? "" : v).trim();
|
|
875
|
+
if (!s) return "";
|
|
876
|
+
if (/^\d{11}$/.test(s)) return s.slice(0, 3) + "****" + s.slice(7);
|
|
877
|
+
if (/^\d{7,}$/.test(s)) return s.slice(0, 3) + "****" + s.slice(-2);
|
|
878
|
+
return s;
|
|
879
|
+
}
|
|
880
|
+
|
|
437
881
|
/** 一次性访问密钥 / 已授权设备列表的自动重试状态(放在组件外:不随渲染重建)。 */
|
|
438
882
|
var akeyRetry = { timer: null, attempt: 0 };
|
|
439
883
|
var devRetry = { timer: null, attempt: 0 };
|
|
@@ -453,6 +897,73 @@ window.__ModuleLoader__.load({
|
|
|
453
897
|
*/
|
|
454
898
|
var connFailRef = { v: 0 };
|
|
455
899
|
var CONN_FAIL_VISIBLE = 3; // ≈ 2.5s × 3:既要够快让用户看见,又要避免一次抖动就报红
|
|
900
|
+
/**
|
|
901
|
+
* 「连接偏慢」的本地观察起点(0 = 不慢)——用于在用户干等时给出「转化 or 拉新」双路块。
|
|
902
|
+
* 只用既有 /dsh-remote/bridge-status 下发字段 + 本地计时,**不新增也不假设后端字段**;
|
|
903
|
+
* 观察不到就永远不显示(宁可不出现,也不误报)。
|
|
904
|
+
*/
|
|
905
|
+
var slowConnTrack = { since: 0 };
|
|
906
|
+
var SLOW_CONNECT_HINT_MS = 90000; // 非 online 阶段持续 90s 才认为「明显偏慢」
|
|
907
|
+
// ── 事实口径常量(🔒 只写线上真值,一个字都不编)─────────────────────────────
|
|
908
|
+
// 邀请奖励:线上 public-config 下发 invite_rule { n: 1, days: 3 }。
|
|
909
|
+
// 此处只在 public-config 取不到时(旧 node 半 / 请求失败)兜底,
|
|
910
|
+
// 用「与线上一致」的值,避免面板显示 3 位/15 天这种和线上对不上的旧口径。
|
|
911
|
+
// 注意:被邀请人**没有任何额外奖励**(trial_days = 0),所以禁止任何「双方都得」的写法。
|
|
912
|
+
var INVITE_RULE_FALLBACK = { n: 1, days: 3 };
|
|
913
|
+
// ── 套餐口径:**先读线上真形状** ─────────────────────────────────────────
|
|
914
|
+
// 线上实测(GET https://n.risegao.cn:13443/relay-api/api/public-config):
|
|
915
|
+
// plans.free = { max_mbps: 1, monthly_gb: 1 }
|
|
916
|
+
// plans.pro = { max_mbps: 5, monthly_gb: 20, devices: 1, monthly_resets: 5 }
|
|
917
|
+
// plans.pro_max = { max_mbps: 10, monthly_gb: 60, devices: 10, online: 3 }
|
|
918
|
+
// prices = { pro: 20, pro_max: 30, ... } ← 价格在**另一个**顶层字段;plans 里没有价格
|
|
919
|
+
// 所以:带宽读 plans[k].max_mbps、流量读 plans[k].monthly_gb、价格读 prices[k]。
|
|
920
|
+
// ⚠️ 早期版本检测的是 { mbps, gb, price } —— 线上这三个键**都不存在**,
|
|
921
|
+
// 于是特性检测永远不命中、面板一直用常量。现在真形状优先,旧形状只作兼容兜底。
|
|
922
|
+
// PLAN_FACTS 仅在**字段缺失或非法**时兜底(值仍与线上一致:PRO ¥20/月 5 Mbps 20 GB)。
|
|
923
|
+
var PLAN_FACTS = {
|
|
924
|
+
pro: { mbps: 5, gb: 20, price: 20, name: "PRO" },
|
|
925
|
+
pro_max: { mbps: 10, gb: 60, price: 30, name: "Pro Max" }
|
|
926
|
+
};
|
|
927
|
+
/** 免费档口径(与 PLAN_FACTS 同源):仅在服务端没下发 plans.free 时兜底。 */
|
|
928
|
+
var FREE_FACTS = { mbps: 1, gb: 1 };
|
|
929
|
+
/**
|
|
930
|
+
* 取第一个**合法数值**:未定义 / null / 空串 / 非数字 → 跳到下一个候选。
|
|
931
|
+
* 全都没命中 → null(由调用方决定兜底)。
|
|
932
|
+
*
|
|
933
|
+
* 🔒 刻意**不用** `Number(x) > 0 ? x : 兜底`:`monthly_gb: 0`(可能表示「不限流量」)
|
|
934
|
+
* 与 `prices.pro: 0`(免费档)都是**合法值**,`> 0` 会把它们吃掉 ——
|
|
935
|
+
* 这与后端 `|| 3 / || 15` 是同一类 bug(0 被当成「没配」)。
|
|
936
|
+
* 这里只判「缺没缺」,不判「大不大」。
|
|
937
|
+
*/
|
|
938
|
+
function firstNum(candidates) {
|
|
939
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
940
|
+
var raw = candidates[i];
|
|
941
|
+
if (raw === undefined || raw === null || raw === "") continue;
|
|
942
|
+
var n = Number(raw);
|
|
943
|
+
if (isFinite(n)) return n;
|
|
944
|
+
}
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* 拉新是否被**显式关闭**:public-config.invite_rule 的 n / days 配成 0(或负数)= 关闭拉新
|
|
949
|
+
* (与 `trial_days = 0 = 关闭` 同一约定)。关闭后所有拉新入口与卡片整体隐藏 ——
|
|
950
|
+
* 既不显示「每 0 位新用户 → 0 天」,也不留一个点进去没内容的入口。
|
|
951
|
+
*
|
|
952
|
+
* 🔒 这里必须**只看原始值**:不能先过 `Number(x) > 0 ? x : 兜底` 那一层,
|
|
953
|
+
* 否则 0 会被兜底值吃掉(后端那个 `|| 3 / || 15` 的 bug 就是这么把关闭态吃成「每 3 人送 15 天」的)。
|
|
954
|
+
*
|
|
955
|
+
* 返回 false(=按显示处理)的情况:public-config 取不到、invite_rule 缺失、字段缺失或非数字
|
|
956
|
+
* —— 判断不了就别把功能藏掉(向后兼容旧 node 半)。
|
|
957
|
+
*/
|
|
958
|
+
function inviteOffFrom(rule) {
|
|
959
|
+
if (!rule || typeof rule !== "object") return false;
|
|
960
|
+
var nRaw = rule.n, dRaw = rule.days;
|
|
961
|
+
var nNum = Number(nRaw), dNum = Number(dRaw);
|
|
962
|
+
var present = function (raw) { return raw !== undefined && raw !== null && raw !== ""; };
|
|
963
|
+
var nOff = present(nRaw) && isFinite(nNum) && nNum <= 0;
|
|
964
|
+
var dOff = present(dRaw) && isFinite(dNum) && dNum <= 0;
|
|
965
|
+
return !!(nOff || dOff);
|
|
966
|
+
}
|
|
456
967
|
/** 连接阶段轮询节奏:未连通时 2.5s(自动推进),已连接后退避到 15s;页面隐藏时完全停下。 */
|
|
457
968
|
var CONN_POLL_FAST_MS = 2500;
|
|
458
969
|
var CONN_POLL_SLOW_MS = 15000;
|
|
@@ -858,6 +1369,9 @@ window.__ModuleLoader__.load({
|
|
|
858
1369
|
}
|
|
859
1370
|
|
|
860
1371
|
function FeedbackPopup() {
|
|
1372
|
+
// 🎨 主题:弹窗是 shell.overlay 下的独立根(不在 .dru-settings-section 里),
|
|
1373
|
+
// 同样要拿一份 data-dru-theme 才有深色令牌;放在 useState 之前不扰动 hook 序号。
|
|
1374
|
+
var theme = useSyncExternalStore(themeSubscribe, themeGet);
|
|
861
1375
|
var open = usePopupOpen();
|
|
862
1376
|
var stepArr = useState("rate"); var step = stepArr[0]; var setStep = stepArr[1];
|
|
863
1377
|
var ratingArr = useState(null); var rating = ratingArr[0]; var setRating = ratingArr[1];
|
|
@@ -878,7 +1392,19 @@ window.__ModuleLoader__.load({
|
|
|
878
1392
|
if (!acct || !acct.invite_code) { setPopupInvite({ code: "" }); return; }
|
|
879
1393
|
api("/dsh-remote/remote-url").then(function (u) {
|
|
880
1394
|
var base = (u && u.remoteUrl) || "https://n.risegao.cn:13443/app/";
|
|
881
|
-
|
|
1395
|
+
// 特性检测:public-config 里带了 invite_rule 就用服务端数值;没带 → 0 = 不显示具体数字。
|
|
1396
|
+
// 🔒 用 firstNum 只判「缺没缺」:`n/days` 配成 0 是合法的「活动关闭」态,
|
|
1397
|
+
// 不能被 `> 0` 这样写吃掉再回落成 1/3(后端 `|| 3 / || 15` 的同款坑)。
|
|
1398
|
+
var pc = (u && u.publicConfig) || null;
|
|
1399
|
+
var raw = (pc && pc.invite_rule) || null;
|
|
1400
|
+
var nNum = firstNum([raw && raw.n]);
|
|
1401
|
+
var dNum = firstNum([raw && raw.days]);
|
|
1402
|
+
setPopupInvite({
|
|
1403
|
+
code: acct.invite_code,
|
|
1404
|
+
link: base.replace(/\/+$/, "") + "/?invite=" + encodeURIComponent(acct.invite_code),
|
|
1405
|
+
n: nNum === null ? 0 : nNum,
|
|
1406
|
+
days: dNum === null ? 0 : dNum
|
|
1407
|
+
});
|
|
882
1408
|
}).catch(function () { setPopupInvite({ code: acct.invite_code }); });
|
|
883
1409
|
}).catch(function () { setPopupInvite({ code: "" }); });
|
|
884
1410
|
}
|
|
@@ -890,6 +1416,13 @@ window.__ModuleLoader__.load({
|
|
|
890
1416
|
api("/dsh-remote/feedback-config").then(function (b) {
|
|
891
1417
|
setFbAuth(b.auth === "account" ? "account" : "anonymous");
|
|
892
1418
|
}).catch(function () { setFbAuth("anonymous"); });
|
|
1419
|
+
// 拉新关闭态探测(public-config 的 invite_rule.n / days = 0 = 关闭):
|
|
1420
|
+
// 只有明确读到「关闭」才记下来 —— 读不到/字段缺失一律不记(判断不了 → 按显示处理)。
|
|
1421
|
+
// 命中关闭时复用 popupInvite 这个既有 state 触发重渲染,同时挡住后面的取码/取链接请求。
|
|
1422
|
+
api("/dsh-remote/remote-url").then(function (u) {
|
|
1423
|
+
var pc = (u && u.publicConfig) || null;
|
|
1424
|
+
if (inviteOffFrom(pc && pc.invite_rule)) setPopupInvite({ code: "", link: "", inviteOff: true });
|
|
1425
|
+
}).catch(function () {});
|
|
893
1426
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
894
1427
|
}, [open]);
|
|
895
1428
|
|
|
@@ -920,7 +1453,7 @@ window.__ModuleLoader__.load({
|
|
|
920
1453
|
.finally(function () { setBusy(""); });
|
|
921
1454
|
};
|
|
922
1455
|
|
|
923
|
-
return h("div", { className: "dru-popup", onMouseDown: function (e) { if (e.target === e.currentTarget) fbPopupLater(); } },
|
|
1456
|
+
return h("div", { className: "dru-popup", "data-dru-theme": theme, onMouseDown: function (e) { if (e.target === e.currentTarget) fbPopupLater(); } },
|
|
924
1457
|
h("div", { className: "dru-popup-card", role: "dialog", "aria-label": "用户反馈" },
|
|
925
1458
|
h("div", { className: "dru-popup-body" },
|
|
926
1459
|
step === "rate"
|
|
@@ -947,31 +1480,47 @@ window.__ModuleLoader__.load({
|
|
|
947
1480
|
: h("div", null,
|
|
948
1481
|
h("div", { className: "dru-popup-icon" }, "🤝"),
|
|
949
1482
|
h("div", { className: "dru-popup-title" }, "愿意推荐给朋友吗?"),
|
|
950
|
-
|
|
1483
|
+
// 拉新关闭态:不承诺任何奖励、不指路邀请页(入口与卡片也一并隐藏)。
|
|
1484
|
+
// 但这一步**整块保留**(否则 recommend 永远为 null,「完成」按钮不出现=死胡同)。
|
|
1485
|
+
popupInvite && popupInvite.inviteOff
|
|
1486
|
+
? h("div", { className: "dru-popup-sub" },
|
|
1487
|
+
"(当前没有推荐奖励活动:带新用户换会员时长已暂时关闭,把 App 分享给朋友就好。)")
|
|
1488
|
+
// 交换句式(不能说「双方都得」:被邀请人没有任何额外奖励,trial_days = 0)
|
|
1489
|
+
: h("div", { className: "dru-popup-sub" },
|
|
1490
|
+
popupInvite && popupInvite.n && popupInvite.days
|
|
1491
|
+
? "带 " + popupInvite.n + " 位新用户 → 你得 " + popupInvite.days + " 天 PRO 会员:对方用你的邀请链接注册新账号 + 在电脑上装好并上线后自动到账。"
|
|
1492
|
+
: "带新用户 → 你得会员时长:对方用你的邀请链接注册新账号 + 在电脑上装好 dsh-remote-web 并上线后自动到账(奖励只发给邀请人)。"),
|
|
951
1493
|
recommend === true
|
|
952
1494
|
? h("div", { className: "dru-popup-invite" },
|
|
953
1495
|
h("div", null, "🎉 感谢推荐!"),
|
|
954
|
-
popupInvite && popupInvite.
|
|
1496
|
+
popupInvite && popupInvite.inviteOff
|
|
1497
|
+
? h("div", { style: { marginTop: 6, fontSize: 12 } }, "当前未开放带新用户换会员时长的活动,暂时不需要邀请链接。")
|
|
1498
|
+
: popupInvite && popupInvite.code && popupInvite.link
|
|
955
1499
|
? h("div", null,
|
|
956
1500
|
h("div", { className: "dru-url", style: { marginTop: 10, textAlign: "left" } },
|
|
957
1501
|
h("span", null, popupInvite.link),
|
|
958
1502
|
h("button", { type: "button", onClick: function () {
|
|
959
|
-
|
|
1503
|
+
copyText(popupInvite.link).then(function (done) {
|
|
1504
|
+
setInviteCopied(done);
|
|
1505
|
+
if (done) later(function () { setInviteCopied(false); }, 2000);
|
|
1506
|
+
});
|
|
960
1507
|
} }, inviteCopied ? "已复制" : "复制邀请链接")
|
|
961
1508
|
),
|
|
962
|
-
h("div", { style: { marginTop: 8, fontSize: 12 } }, "
|
|
1509
|
+
h("div", { style: { marginTop: 8, fontSize: 12 } }, "把链接发给一位还没注册过的新用户,对方注册时会自动带上你的邀请码。")
|
|
963
1510
|
)
|
|
964
1511
|
: h("div", { style: { marginTop: 6, fontSize: 12 } },
|
|
965
1512
|
popupInvite && popupInvite.code
|
|
966
|
-
? "邀请码 " + popupInvite.code + " 已生成:在设置面板
|
|
967
|
-
: "登录手机号账号后,在设置面板
|
|
1513
|
+
? "邀请码 " + popupInvite.code + " 已生成:在设置面板 →「🎁 带新用户,换会员时长」里复制邀请链接。"
|
|
1514
|
+
: "登录手机号账号后,在设置面板 →「🎁 带新用户,换会员时长」里获取专属邀请链接。")
|
|
968
1515
|
)
|
|
969
1516
|
: h("div", null,
|
|
970
1517
|
h("div", { className: "dru-popup-actions" },
|
|
971
1518
|
h("button", { type: "button", className: "dru-btn dru-btn-primary", onClick: function () { setRecommend(true); loadPopupInvite(); } }, "愿意推荐"),
|
|
972
1519
|
h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: function () { setRecommend(false); } }, "暂时不了")
|
|
973
1520
|
),
|
|
974
|
-
|
|
1521
|
+
popupInvite && popupInvite.inviteOff
|
|
1522
|
+
? null
|
|
1523
|
+
: h("div", { className: "dru-hint", style: { marginTop: 10 } }, "推荐成功可获得你的专属邀请链接(带新用户换会员时长)")
|
|
975
1524
|
),
|
|
976
1525
|
message && h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text),
|
|
977
1526
|
recommend !== null && h("div", { className: "dru-popup-actions" },
|
|
@@ -992,10 +1541,50 @@ window.__ModuleLoader__.load({
|
|
|
992
1541
|
// (原 🖥 已随入口迁移调整);栏目导航 label 与栏目头部均使用它。
|
|
993
1542
|
|
|
994
1543
|
// ── 版本与更新卡片(自管理:市场没有更新按钮,这里提供在线一键更新/彻底卸载) ──
|
|
995
|
-
// 数据来自 node
|
|
1544
|
+
// 数据来自 node 半的 /dsh-remote/self* 路由;逻辑均在插件 node 半实现,
|
|
996
1545
|
// 因此无论插件从「插件市场」还是 npx 安装,界面与行为完全一致。
|
|
1546
|
+
//
|
|
1547
|
+
// 【0.6.9 冻结契约】——**每个新字段都必须特性检测**,缺字段要优雅降级
|
|
1548
|
+
// (否则面板会在旧 node 半上白屏/报错):
|
|
1549
|
+
// GET /dsh-remote/self → { version, runtimeReady, channel? }
|
|
1550
|
+
// GET /dsh-remote/self/update-log → { running, log, failure?, channel?,
|
|
1551
|
+
// startedAt?, elapsedMs?, idleMs?, idleThresholdMs?, stalled? }
|
|
1552
|
+
// POST /dsh-remote/self/update → { ok, channel? }
|
|
1553
|
+
// POST /dsh-remote/self/update/cancel → { ok, killed, cleared }(旧 node 半无此路由 → 404 → 隐藏按钮并给一句人话)
|
|
1554
|
+
// failure.failCode 可能是 update_stalled(看门狗判定无输出)/ user_cancelled(用户主动取消)等
|
|
1555
|
+
// 「已用时 / 无输出时长」在字段缺失时用本地观测兜底(startedAt/日志变化时间),保证旧宿主也看得见。
|
|
1556
|
+
var UPDATE_IDLE_HINT_MS = 20000; // 无新日志超过 20s → 灰字提示(不惊动用户)
|
|
1557
|
+
var UPDATE_CANCEL_IDLE_MS = 90000; // 超过 90s 无变化 → 出现「取消并重试」(旧宿主无 idleThresholdMs 时的兜底)
|
|
1558
|
+
/** 更新通道 → 人话(channel 也可能是具体版本号)。 */
|
|
1559
|
+
var UPDATE_CHANNEL_COPY = { latest: "稳定版", beta: "预览版", next: "预览版", rc: "候选版" };
|
|
1560
|
+
function updateChannelText(ch) {
|
|
1561
|
+
var c = String(ch || "").trim();
|
|
1562
|
+
if (!c) return "";
|
|
1563
|
+
var friendly = UPDATE_CHANNEL_COPY[c];
|
|
1564
|
+
if (friendly) return friendly + "(" + c + ")";
|
|
1565
|
+
if (/^\d/.test(c)) return "指定版本(" + c + ")";
|
|
1566
|
+
return c;
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* fail_code → 人话补一句。**只映射能确定的码**;未知码返回空串(不编造原因,
|
|
1570
|
+
* 由服务端 detail 说明)。update_stalled 是 0.6.9 看门狗新增:更新进程长时间无输出。
|
|
1571
|
+
*/
|
|
1572
|
+
var UPDATE_FAIL_COPY = {
|
|
1573
|
+
update_stalled: "更新进程长时间没有响应,已自动换源重试;若长时间没有变化,可点「取消并重试」。",
|
|
1574
|
+
user_cancelled: "更新已被取消,可以重新点「一键更新」再试一次。",
|
|
1575
|
+
npx_cmd_unavailable: "本机找不到可用的安装命令(npx):请先安装 Node.js,或重启一次 dsh web 后重试。",
|
|
1576
|
+
node_missing: "本机没有找到 Node.js:请先安装 Node.js 后重试。",
|
|
1577
|
+
node_too_old: "本机 Node.js 版本过低(需要 20 以上):升级 Node.js 后重试。",
|
|
1578
|
+
npm_unreachable: "连不上软件源:请检查网络后重试(面板会自动换源)。",
|
|
1579
|
+
registry_timeout: "软件源响应超时:稍后重试即可(面板会自动换源)。",
|
|
1580
|
+
platform_unsupported: "当前系统不支持自动更新:请按 README 手动安装。",
|
|
1581
|
+
runtime_install_timeout: "运行环境安装超时:已自动重试,仍失败可点「取消并重试」。",
|
|
1582
|
+
};
|
|
1583
|
+
/** 更新进度的本地兜底(旧 node 半不下发进度字段时用)——放组件外,跨渲染保留。 */
|
|
1584
|
+
var updTrack = { startedAt: 0, lastChangeAt: 0, lastLog: "" };
|
|
1585
|
+
|
|
997
1586
|
function SelfManageCard() {
|
|
998
|
-
var verArr = useState(null); var ver = verArr[0]; var setVer = verArr[1]; // {version, runtimeReady}
|
|
1587
|
+
var verArr = useState(null); var ver = verArr[0]; var setVer = verArr[1]; // {version, channel?, runtimeReady}
|
|
999
1588
|
var chkArr = useState(null); var chk = chkArr[0]; var setChk = chkArr[1]; // {current, latest, outdated}
|
|
1000
1589
|
var chkBusyArr = useState(false); var chkBusy = chkBusyArr[0]; var setChkBusy = chkBusyArr[1];
|
|
1001
1590
|
var upBusyArr = useState(false); var upBusy = upBusyArr[0]; var setUpBusy = upBusyArr[1];
|
|
@@ -1005,6 +1594,12 @@ window.__ModuleLoader__.load({
|
|
|
1005
1594
|
var doneArr = useState(false); var updated = doneArr[0]; var setUpdated = doneArr[1]; // 本轮已更新完成(提示重启)
|
|
1006
1595
|
var armArr = useState(false); var armed = armArr[0]; var setArmed = armArr[1]; // 彻底卸载二次确认
|
|
1007
1596
|
var msgArr = useState(null); var selfMsg = msgArr[0]; var setSelfMsg = msgArr[1]; // {kind, text}
|
|
1597
|
+
// 【0.6.9】更新可见性:进度快照(已用时/无输出/是否卡住)+ 取消按钮状态
|
|
1598
|
+
var progArr = useState(null); var prog = progArr[0]; var setProg = progArr[1];
|
|
1599
|
+
var tickArr = useState(0); var updTick = tickArr[0]; var setTick = tickArr[1]; // 1s 心跳:让「已用时」平滑走字
|
|
1600
|
+
var cxBusyArr = useState(false); var cxBusy = cxBusyArr[0]; var setCxBusy = cxBusyArr[1];
|
|
1601
|
+
var cxOkArr = useState(null); var cancelOk = cxOkArr[0]; var setCancelOk = cxOkArr[1]; // null=未知 true=可用 false=旧宿主无此接口
|
|
1602
|
+
var cxMsgArr = useState(null); var cxMsg = cxMsgArr[0]; var setCxMsg = cxMsgArr[1]; // {kind, text}
|
|
1008
1603
|
|
|
1009
1604
|
var loadVer = useCallback(function () {
|
|
1010
1605
|
api("/dsh-remote/self").then(function (b) {
|
|
@@ -1022,59 +1617,98 @@ window.__ModuleLoader__.load({
|
|
|
1022
1617
|
}).finally(function () { setChkBusy(false); });
|
|
1023
1618
|
}, []);
|
|
1024
1619
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1620
|
+
/**
|
|
1621
|
+
* 处理一次 /self/update-log 响应:刷新日志、进度指标、失败原因与版本;
|
|
1622
|
+
* running=true → 继续跟踪;running=false → 收尾(进程结束 ≠ 更新成功)。
|
|
1623
|
+
*/
|
|
1624
|
+
var applyUpdLog = useCallback(function (b) {
|
|
1625
|
+
if (!b || !b.ok) return;
|
|
1626
|
+
var now = Date.now();
|
|
1627
|
+
var nextLog = typeof b.log === "string" ? b.log : "";
|
|
1628
|
+
if (nextLog !== updTrack.lastLog) { updTrack.lastLog = nextLog; updTrack.lastChangeAt = now; }
|
|
1629
|
+
setLog(nextLog);
|
|
1630
|
+
var running = !!b.running;
|
|
1631
|
+
var startedAt = Number(b.startedAt) > 0 ? Number(b.startedAt) : 0;
|
|
1632
|
+
if (running && !updTrack.startedAt) updTrack.startedAt = startedAt || now;
|
|
1633
|
+
if (!running) updTrack.startedAt = 0;
|
|
1634
|
+
var hasIdle = isFinite(Number(b.idleMs));
|
|
1635
|
+
setProg({
|
|
1636
|
+
running: running,
|
|
1637
|
+
at: now,
|
|
1638
|
+
startedAt: startedAt || updTrack.startedAt || 0,
|
|
1639
|
+
elapsedMs: isFinite(Number(b.elapsedMs)) ? Number(b.elapsedMs) : 0,
|
|
1640
|
+
// 旧宿主没有 idleMs → 用「日志最后一次变化的本地时间」兜底
|
|
1641
|
+
idleMs: hasIdle ? Number(b.idleMs) : (running && updTrack.lastChangeAt ? now - updTrack.lastChangeAt : 0),
|
|
1642
|
+
idleThresholdMs: isFinite(Number(b.idleThresholdMs)) ? Number(b.idleThresholdMs) : 0,
|
|
1643
|
+
stalled: b.stalled === true,
|
|
1644
|
+
channel: typeof b.channel === "string" ? b.channel : ""
|
|
1645
|
+
});
|
|
1646
|
+
if (running) { setUpdating(true); return; }
|
|
1647
|
+
setUpdating(false);
|
|
1648
|
+
// 关键:进程结束 ≠ 更新成功。以前这里无条件显示"已更新完成",
|
|
1649
|
+
// 于是 spawn 失败(例如 npx 不可用)时界面只报成功、真实原因躺在日志里 ——
|
|
1650
|
+
// 用户看到的就是"点了一键修复没反应"。现在按失败信息与版本是否变化如实反馈。
|
|
1651
|
+
if (b.failure && b.failure.failCode === "user_cancelled") {
|
|
1652
|
+
// 用户自己点的取消:不是错误,用中性提示,不刷红字
|
|
1653
|
+
setUpdated(false);
|
|
1654
|
+
setSelfMsg({ kind: "warn", text: UPDATE_FAIL_COPY.user_cancelled });
|
|
1655
|
+
} else if (b.failure && b.failure.detail) {
|
|
1656
|
+
setUpdated(false);
|
|
1657
|
+
setSelfMsg({
|
|
1658
|
+
kind: "err",
|
|
1659
|
+
text: "更新失败:" + b.failure.detail + failCodeHint(b.failure.failCode, b.failure.detail) + "(可查看下方日志,或点「一键更新」重试)"
|
|
1660
|
+
});
|
|
1661
|
+
} else {
|
|
1662
|
+
setUpdated(true);
|
|
1663
|
+
}
|
|
1664
|
+
loadVer();
|
|
1665
|
+
doCheck();
|
|
1666
|
+
}, [loadVer, doCheck]);
|
|
1041
1667
|
|
|
1042
|
-
|
|
1668
|
+
/**
|
|
1669
|
+
* fail_code → 人话(未知码返回空串:不编造原因,交给服务端 detail)。
|
|
1670
|
+
* detail 已经说了同一件事时不重复刷一遍(0.6.9 的 update_stalled 就属于这种情况)。
|
|
1671
|
+
*/
|
|
1672
|
+
function failCodeHint(code, detail) {
|
|
1673
|
+
var txt = UPDATE_FAIL_COPY[String(code || "")];
|
|
1674
|
+
if (!txt) return "";
|
|
1675
|
+
var key = txt.split(/[,;。]/)[0];
|
|
1676
|
+
if (detail && key && String(detail).indexOf(key) !== -1) return "";
|
|
1677
|
+
return " | " + txt;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// 一次性拉取更新状态:点击更新后立刻同步一次;面板打开时也探一次(更新可能是在别处点起的)
|
|
1681
|
+
var logTimer = useCallback(function () {
|
|
1682
|
+
return api("/dsh-remote/self/update-log").then(function (b) {
|
|
1683
|
+
applyUpdLog(b);
|
|
1684
|
+
}).catch(function () { /* 探针失败不影响面板 */ });
|
|
1685
|
+
}, [applyUpdLog]);
|
|
1686
|
+
|
|
1687
|
+
useEffect(function () { loadVer(); doCheck(); logTimer(); }, [loadVer, doCheck, logTimer]);
|
|
1688
|
+
// 更新进行中:每 2s 拉日志+进度;结束(running=false)时由 applyUpdLog 收尾并清掉定时器
|
|
1043
1689
|
useEffect(function () {
|
|
1044
|
-
if (!updating) return;
|
|
1045
|
-
var iv = setInterval(function () {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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);
|
|
1690
|
+
if (!updating) return undefined;
|
|
1691
|
+
var iv = setInterval(function () { logTimer(); }, 2000);
|
|
1692
|
+
return function () { clearInterval(iv); };
|
|
1693
|
+
}, [updating, logTimer]);
|
|
1694
|
+
// 「已用时」走字:只在更新中开 1s 心跳(尊重 reduced-motion 也不影响,纯文本更新)
|
|
1695
|
+
useEffect(function () {
|
|
1696
|
+
if (!updating) return undefined;
|
|
1697
|
+
var iv = setInterval(function () { setTick(function (v) { return v + 1; }); }, 1000);
|
|
1066
1698
|
return function () { clearInterval(iv); };
|
|
1067
|
-
}, [updating
|
|
1699
|
+
}, [updating]);
|
|
1700
|
+
// 注:updTick 只用于触发重渲染(setTick 的引用是稳定的),不进依赖列表
|
|
1068
1701
|
|
|
1069
1702
|
var doUpdate = function () {
|
|
1070
1703
|
setUpBusy(true);
|
|
1071
1704
|
setUpdated(false);
|
|
1072
1705
|
setSelfMsg(null);
|
|
1706
|
+
setCxMsg(null);
|
|
1073
1707
|
post("/dsh-remote/self/update", {}).then(function (b) {
|
|
1074
1708
|
if (b && b.ok) {
|
|
1075
|
-
setSelfMsg({ kind: "ok", text: "
|
|
1709
|
+
setSelfMsg({ kind: "ok", text: "更新已在后台开始,正在下载安装…(本页会实时显示进度日志、已用时与通道)" });
|
|
1076
1710
|
setUpdating(true);
|
|
1077
|
-
logTimer(
|
|
1711
|
+
logTimer();
|
|
1078
1712
|
} else {
|
|
1079
1713
|
var detail = String((b && (b.detail || b.error)) || "更新启动失败");
|
|
1080
1714
|
// 另一种常见情况:node 半返回 ok:false + “已有更新在进行中” → 转为跟踪进度而非报错
|
|
@@ -1082,7 +1716,7 @@ window.__ModuleLoader__.load({
|
|
|
1082
1716
|
if (lb && lb.ok && lb.running) {
|
|
1083
1717
|
setSelfMsg({ kind: "ok", text: "检测到已有一次更新正在进行,正在跟踪进度…" });
|
|
1084
1718
|
setUpdating(true);
|
|
1085
|
-
logTimer(
|
|
1719
|
+
logTimer();
|
|
1086
1720
|
} else {
|
|
1087
1721
|
setSelfMsg({ kind: "err", text: detail });
|
|
1088
1722
|
}
|
|
@@ -1093,6 +1727,40 @@ window.__ModuleLoader__.load({
|
|
|
1093
1727
|
}).finally(function () { setUpBusy(false); });
|
|
1094
1728
|
};
|
|
1095
1729
|
|
|
1730
|
+
/**
|
|
1731
|
+
* 取消更新:POST /dsh-remote/self/update/cancel(结束挂死的更新进程 + 清标记)。
|
|
1732
|
+
* retry=true → 取消成功后立刻重新点一次「一键更新」(即按钮上的「取消并重试」)。
|
|
1733
|
+
* 旧 node 半没有这个路由 → 404 → 隐藏按钮并给一句能照做的话(不把用户留在死胡同)。
|
|
1734
|
+
*/
|
|
1735
|
+
var doCancelUpdate = function (retry) {
|
|
1736
|
+
setCxBusy(true);
|
|
1737
|
+
setCxMsg(null);
|
|
1738
|
+
post("/dsh-remote/self/update/cancel", {}).then(function (b) {
|
|
1739
|
+
if (!b || !b.ok) throw new Error((b && (b.detail || b.error)) || "取消失败");
|
|
1740
|
+
setCancelOk(true);
|
|
1741
|
+
setUpdating(false);
|
|
1742
|
+
setUpdated(false);
|
|
1743
|
+
setProg(null);
|
|
1744
|
+
updTrack.startedAt = 0;
|
|
1745
|
+
setCxMsg({
|
|
1746
|
+
kind: b.cleared === false ? "warn" : "ok",
|
|
1747
|
+
text: b.cleared === false
|
|
1748
|
+
? "取消请求已发出,但更新标记没能清掉:请刷新页面;若仍显示「正在更新」,重启一次 dsh web 即可。"
|
|
1749
|
+
: ("已取消本次更新" + (b.killed === true ? "(更新进程已结束)" : "") + (retry ? ",正在重新开始…" : ",可以重新点「一键更新」再试一次。"))
|
|
1750
|
+
});
|
|
1751
|
+
loadVer();
|
|
1752
|
+
if (retry && b.cleared !== false) doUpdate();
|
|
1753
|
+
}).catch(function (e) {
|
|
1754
|
+
if (e && e.status === 404) {
|
|
1755
|
+
// 旧宿主:没有取消接口 → 永久隐藏该按钮,只留人话提示
|
|
1756
|
+
setCancelOk(false);
|
|
1757
|
+
setCxMsg({ kind: "warn", text: "当前插件版本还不支持一键取消更新:请刷新页面;若仍卡在「正在更新」,重启一次 dsh web 后重新点「一键更新」即可。" });
|
|
1758
|
+
} else {
|
|
1759
|
+
setCxMsg({ kind: "err", text: "取消失败:" + ((e && e.message) || "未知错误") + "(更新进程可能仍在运行;可稍后重试,或重启一次 dsh web)" });
|
|
1760
|
+
}
|
|
1761
|
+
}).finally(function () { setCxBusy(false); });
|
|
1762
|
+
};
|
|
1763
|
+
|
|
1096
1764
|
var doUninstall = function () {
|
|
1097
1765
|
if (!armed) { setArmed(true); return; }
|
|
1098
1766
|
setUnBusy(true);
|
|
@@ -1105,7 +1773,7 @@ window.__ModuleLoader__.load({
|
|
|
1105
1773
|
// 优先展示服务端 detail(含 bridge 自启动/配置目录的逐项清理结果与重启提示);
|
|
1106
1774
|
// 兜底文案同样说明 bridge 自启动服务与本地配置目录会一并移除/清空
|
|
1107
1775
|
var detail = b && b.detail ? String(b.detail) : "";
|
|
1108
|
-
setSelfMsg({ kind: "ok", text: detail || "已彻底卸载:插件引用、bridge 自启动服务与本地配置目录(账号/密钥/运行时等)已一并移除并清空。请重启 dsh web
|
|
1776
|
+
setSelfMsg({ kind: "ok", text: detail || "已彻底卸载:插件引用、bridge 自启动服务与本地配置目录(账号/密钥/运行时等)已一并移除并清空。请重启 dsh web 后完全生效(本栏目将消失);如需再次使用,在插件市场搜索「dsh-remote-web」重新安装,或执行 npx @mrrisega/dsh-remote。" });
|
|
1109
1777
|
} else {
|
|
1110
1778
|
setArmed(false);
|
|
1111
1779
|
setSelfMsg({ kind: "err", text: "卸载失败:" + ((b && (b.error || b.detail)) || "未知错误") });
|
|
@@ -1119,19 +1787,67 @@ window.__ModuleLoader__.load({
|
|
|
1119
1787
|
var outdated = !!(chk && chk.outdated && chk.latest && chk.latest !== chk.current);
|
|
1120
1788
|
var currentV = (ver && ver.version) || (chk && chk.current) || "…";
|
|
1121
1789
|
var runtimeReady = ver ? !!ver.runtimeReady : null;
|
|
1790
|
+
// 通道:GET /self 的 channel 优先(字段可能不存在 → 不渲染任何通道 chip,旧宿主照常工作)
|
|
1791
|
+
var channel = (ver && typeof ver.channel === "string" && ver.channel) || (prog && prog.channel) || "";
|
|
1792
|
+
// 进度指标(字段缺失时用本地观测兜底):已用时 / 无输出时长 / 是否卡住
|
|
1793
|
+
var progRunning = !!(prog && prog.running);
|
|
1794
|
+
var elapsedBase = prog && prog.elapsedMs > 0 ? prog.elapsedMs
|
|
1795
|
+
: (prog && prog.startedAt ? Math.max(0, Date.now() - prog.startedAt) : 0);
|
|
1796
|
+
var elapsedShown = prog && prog.at ? elapsedBase + Math.max(0, Date.now() - prog.at) : elapsedBase;
|
|
1797
|
+
var idleShown = prog ? prog.idleMs + (prog.at ? Math.max(0, Date.now() - prog.at) : 0) : 0;
|
|
1798
|
+
var cancelAfterMs = prog && prog.idleThresholdMs > 0 ? prog.idleThresholdMs : UPDATE_CANCEL_IDLE_MS;
|
|
1799
|
+
// 「卡住」只采信服务端判定(stalled=true);旧宿主没有该字段时退化为下面的「无输出」灰字提示,
|
|
1800
|
+
// 不自己编一个「卡住」结论(避免误报)。
|
|
1801
|
+
var stalled = !!(prog && prog.stalled);
|
|
1802
|
+
var canCancel = updating && cancelOk !== false && ((prog && prog.stalled) || idleShown >= cancelAfterMs);
|
|
1803
|
+
var idleHint = progRunning && !stalled && idleShown >= UPDATE_IDLE_HINT_MS;
|
|
1122
1804
|
|
|
1123
1805
|
return h("div", { className: "dru-card", style: { marginTop: 2 } },
|
|
1124
1806
|
h("h3", null, "🔄 版本与更新"),
|
|
1125
|
-
|
|
1126
|
-
|
|
1807
|
+
// ① 版本行:版本号(大字)+ 通道 chip + 新旧状态
|
|
1808
|
+
h("div", { className: "dru-ver-head" },
|
|
1809
|
+
h("span", { className: "dru-ver-num" }, "插件 v" + currentV),
|
|
1810
|
+
channel ? h("span", { className: "dru-ver-chip", title: "当前更新通道:" + channel }, "通道 " + updateChannelText(channel)) : null,
|
|
1127
1811
|
chk === null && chkBusy ? h("span", { className: "dru-meta", style: { margin: 0 } }, "(检查新版本中…)") : null,
|
|
1128
1812
|
chk && outdated
|
|
1129
1813
|
? h("span", { className: "dru-ver-badge dru-ver-badge-new" }, "发现新版本 v" + chk.latest)
|
|
1130
1814
|
: chk && !outdated ? h("span", { className: "dru-ver-badge dru-ver-badge-ok" }, "已是最新版本") : null
|
|
1131
1815
|
),
|
|
1132
1816
|
h("div", { className: "dru-meta" },
|
|
1133
|
-
runtimeReady === false
|
|
1817
|
+
runtimeReady === false
|
|
1818
|
+
? "⚠ 后台服务运行环境缺失(点下方「一键更新」会自动补齐并启动,不用手动装)"
|
|
1819
|
+
: runtimeReady === true ? "后台服务运行环境正常" : "正在读取运行环境…"
|
|
1134
1820
|
),
|
|
1821
|
+
// ② 更新进行中:进度指标(已用时 / 距上次输出)+ 卡住告警(0.6.9 看门狗冻结契约)
|
|
1822
|
+
updating || progRunning
|
|
1823
|
+
? h("div", { className: "dru-ver-metrics", role: "status" },
|
|
1824
|
+
h("div", { className: "dru-ver-metric" },
|
|
1825
|
+
h("span", null, "更新状态"),
|
|
1826
|
+
h("span", null, h("span", { className: "dru-spin", "aria-hidden": "true" }), " 正在进行")
|
|
1827
|
+
),
|
|
1828
|
+
h("div", { className: "dru-ver-metric" }, h("span", null, "已用时"), h("span", null, fmtDuration(elapsedShown))),
|
|
1829
|
+
h("div", { className: "dru-ver-metric" },
|
|
1830
|
+
h("span", null, "距上次输出"),
|
|
1831
|
+
h("span", null, idleShown < 1500 ? "刚刚" : fmtDuration(idleShown))
|
|
1832
|
+
),
|
|
1833
|
+
channel ? h("div", { className: "dru-ver-metric" }, h("span", null, "更新通道"), h("span", null, channel)) : null
|
|
1834
|
+
)
|
|
1835
|
+
: null,
|
|
1836
|
+
stalled
|
|
1837
|
+
? h("div", { className: "dru-stall", role: "alert" },
|
|
1838
|
+
h("b", null, "疑似卡住"),
|
|
1839
|
+
"(已 " + Math.round(idleShown / 1000) + " 秒无输出),正在自动换源重试…",
|
|
1840
|
+
"已经等了 " + fmtDuration(elapsedShown) + ";若长时间没有变化,可点下方「取消并重试」。",
|
|
1841
|
+
cancelOk === false
|
|
1842
|
+
? h("div", { style: { marginTop: 6 } },
|
|
1843
|
+
"当前插件版本还不支持一键取消:可刷新页面;若仍卡在「正在更新」,重启一次 dsh web 后重新点「一键更新」即可。")
|
|
1844
|
+
: null
|
|
1845
|
+
)
|
|
1846
|
+
: idleHint
|
|
1847
|
+
? h("div", { className: "dru-hint", style: { marginTop: 8 } },
|
|
1848
|
+
"已 " + Math.round(idleShown / 1000) + " 秒没有新输出(总计 " + fmtDuration(elapsedShown) + "):安装过程偶尔会安静一会儿,超过 " + Math.round(cancelAfterMs / 1000) + " 秒没变化会出现「取消并重试」。")
|
|
1849
|
+
: null,
|
|
1850
|
+
// ③ 操作区(主操作 / 检查 / 取消并重试 / 彻底卸载)
|
|
1135
1851
|
h("div", { className: "dru-actions", style: { marginTop: 10 } },
|
|
1136
1852
|
outdated
|
|
1137
1853
|
? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: upBusy || unBusy || chkBusy || updating, onClick: doUpdate },
|
|
@@ -1139,6 +1855,15 @@ window.__ModuleLoader__.load({
|
|
|
1139
1855
|
: h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: upBusy || unBusy || chkBusy || updating, onClick: doUpdate },
|
|
1140
1856
|
updating ? "正在更新…" : (ver && !ver.runtimeReady) ? "安装并启动(一键修复)" : "重新检查 / 修复"),
|
|
1141
1857
|
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: chkBusy || updating || upBusy, onClick: doCheck }, chkBusy ? "检查中…" : "检查更新"),
|
|
1858
|
+
canCancel
|
|
1859
|
+
? h("button", {
|
|
1860
|
+
type: "button",
|
|
1861
|
+
className: "dru-btn dru-btn-danger",
|
|
1862
|
+
disabled: cxBusy || upBusy,
|
|
1863
|
+
title: "结束卡住的更新进程并清除更新标记,然后重新开始一次",
|
|
1864
|
+
onClick: function () { doCancelUpdate(true); }
|
|
1865
|
+
}, cxBusy ? "取消中…" : "取消并重试")
|
|
1866
|
+
: null,
|
|
1142
1867
|
h("button", {
|
|
1143
1868
|
type: "button",
|
|
1144
1869
|
className: "dru-btn dru-btn-danger",
|
|
@@ -1147,15 +1872,17 @@ window.__ModuleLoader__.load({
|
|
|
1147
1872
|
onClick: doUninstall
|
|
1148
1873
|
}, unBusy ? "卸载中…" : armed ? "⚠ 再点一次确认彻底卸载" : "彻底卸载")
|
|
1149
1874
|
),
|
|
1875
|
+
cxMsg ? h("div", { className: "dru-msg dru-msg-" + cxMsg.kind }, cxMsg.text) : null,
|
|
1150
1876
|
updated
|
|
1151
1877
|
? h("div", { className: "dru-msg dru-msg-ok" },
|
|
1152
|
-
"✅ 更新已完成,最新代码已就位。请", h("strong", null, "重启 dsh web"), "
|
|
1878
|
+
"✅ 更新已完成,最新代码已就位。请", h("strong", null, "重启 dsh web"), "后生效;后台服务会随系统自启自动运行新版本。")
|
|
1153
1879
|
: null,
|
|
1880
|
+
// ④ 更新日志(失败时同样靠近上面的错误提示)
|
|
1154
1881
|
log ? h("div", { className: "dru-up-log", title: "更新日志(尾部)" }, log) : null,
|
|
1155
1882
|
selfMsg ? h("div", { className: "dru-msg dru-msg-" + selfMsg.kind }, selfMsg.text) : null,
|
|
1156
1883
|
h("div", { className: "dru-hint", style: { marginTop: 8 } },
|
|
1157
|
-
armed ? "⚠ 再次点击后即开始彻底卸载:① 移除 dsh web 配置中的插件引用与本地文件;② 停止并移除 bridge
|
|
1158
|
-
"
|
|
1884
|
+
armed ? "⚠ 再次点击后即开始彻底卸载:① 移除 dsh web 配置中的插件引用与本地文件;② 停止并移除 bridge 自启动服务(即让「后台服务」不再开机自启:macOS com.dshremote.bridge / Linux dsh-bridge / Windows 任务计划程序 dsh-remote-bridge)并结束残留进程;③ 清空本地配置目录(~/.dsh-remote:账号、设备密钥、固化运行时等)。此操作不可撤销,如需再次使用:在插件市场搜索「dsh-remote-web」重新安装,或执行 npx @mrrisega/dsh-remote。" :
|
|
1885
|
+
"检测新版、一键在线更新、彻底卸载都在本卡片完成;更新卡住时会出现「取消并重试」。")
|
|
1159
1886
|
);
|
|
1160
1887
|
}
|
|
1161
1888
|
|
|
@@ -1183,12 +1910,18 @@ window.__ModuleLoader__.load({
|
|
|
1183
1910
|
// 匿名装机统计的「面板打开」只上报一次/每页(node 半还会每进程去重)。
|
|
1184
1911
|
var panelOpenedSent = false;
|
|
1185
1912
|
function RemoteControlSection(props) {
|
|
1913
|
+
// 🎨 主题(宿主深色/亮色):订阅外部存储 → 宿主切换时本组件自动重渲染,
|
|
1914
|
+
// 根节点上的 data-dru-theme 随之更新,整套 --dru-* 令牌实时切换。
|
|
1915
|
+
// 放在全部 useState 之前:不改变既有 hook 序号(useSyncExternalStore 与 useState 各自计数)。
|
|
1916
|
+
var theme = useSyncExternalStore(themeSubscribe, themeGet);
|
|
1186
1917
|
|
|
1187
1918
|
var statusArr = useState(null); var st = statusArr[0]; var setSt = statusArr[1];
|
|
1188
1919
|
var modeArr = useState("saas"); var mode = modeArr[0]; var setMode = modeArr[1]; // saas | local
|
|
1189
1920
|
var viewArr = useState("home"); var view = viewArr[0]; var setView = viewArr[1]; // home | feedback | invite
|
|
1190
1921
|
var busyArr = useState(""); var busy = busyArr[0]; var setBusy = busyArr[1];
|
|
1191
1922
|
var msgArr = useState(null); var message = msgArr[0]; var setMessage = msgArr[1];
|
|
1923
|
+
// 注:邀请区复制反馈已迁到 viewstate 末端的 inviteCopied(0.6.9);这个 hook 保留占位,
|
|
1924
|
+
// 避免后续所有 hook 序号前移(本组件大量 useCallback/useEffect 依赖稳定序号)。
|
|
1192
1925
|
var copiedArr = useState(false); var copied = copiedArr[0]; var setCopied = copiedArr[1];
|
|
1193
1926
|
|
|
1194
1927
|
// 我的信息(个人中心)数据
|
|
@@ -1253,6 +1986,16 @@ window.__ModuleLoader__.load({
|
|
|
1253
1986
|
var connArr = useState(null); var conn = connArr[0]; var setConn = connArr[1];
|
|
1254
1987
|
var copiedDiagArr = useState(false); var copiedDiag = copiedDiagArr[0]; var setCopiedDiag = copiedDiagArr[1];
|
|
1255
1988
|
|
|
1989
|
+
// ── 🎁 邀请视图(带新用户换会员)状态:放在全部既有字段之后,保持既有 hook 序号不变 ──
|
|
1990
|
+
var invLoadArr = useState(false); var inviteLoading = invLoadArr[0]; var setInviteLoading = invLoadArr[1]; // 记录/规则读取中
|
|
1991
|
+
var invErrArr = useState(null); var inviteErr = invErrArr[0]; var setInviteErr = invErrArr[1]; // 记录读取失败原因(≠空记录)
|
|
1992
|
+
var invRulesArr = useState(false); var inviteRulesOpen = invRulesArr[0]; var setInviteRulesOpen = invRulesArr[1]; // 活动规则默认折叠
|
|
1993
|
+
var invCopyArr = useState(""); var inviteCopied = invCopyArr[0]; var setInviteCopied = invCopyArr[1]; // "" | "code" | "link"(复制成功反馈)
|
|
1994
|
+
var invCopyErrArr = useState(null); var inviteCopyErr = invCopyErrArr[0]; var setInviteCopyErr = invCopyErrArr[1]; // 复制失败原因(就近提示)
|
|
1995
|
+
// 🎁 邀请收益引导气泡的「已关闭」态(同样追加在末尾,不动既有 hook 序号)。
|
|
1996
|
+
// 初值直接读 localStorage:已关闭过就不再出现,不闪一下再消失。
|
|
1997
|
+
var tipDismissedArr = useState(inviteTipWasDismissed()); var inviteTipDismissed = tipDismissedArr[0]; var setInviteTipDismissed = tipDismissedArr[1];
|
|
1998
|
+
|
|
1256
1999
|
var refresh = useCallback(function () {
|
|
1257
2000
|
setBusy("status");
|
|
1258
2001
|
api("/dsh-remote/status").then(function (body) {
|
|
@@ -1417,12 +2160,15 @@ window.__ModuleLoader__.load({
|
|
|
1417
2160
|
|
|
1418
2161
|
var copyKeyUrl = function () {
|
|
1419
2162
|
if (!(akey && akey.url)) return;
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
2163
|
+
// 统一走 copyText:剪贴板不可用(http 非安全上下文)时退回 execCommand;都失败就给一句人话
|
|
2164
|
+
copyText(akey.url).then(function (done) {
|
|
2165
|
+
if (!done) {
|
|
2166
|
+
setAkeyMsg({ kind: "err", text: "复制失败:浏览器可能限制了剪贴板权限,请手动选中上面的链接复制。" });
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
setCopiedKey(true);
|
|
2170
|
+
later(function () { setCopiedKey(false); }, 2000);
|
|
2171
|
+
});
|
|
1426
2172
|
};
|
|
1427
2173
|
/** 「直接打开」:浏览器新标签打开一次性访问地址(打开即扫码/点击进入)。 */
|
|
1428
2174
|
var openKeyUrl = function () {
|
|
@@ -1689,8 +2435,8 @@ window.__ModuleLoader__.load({
|
|
|
1689
2435
|
text: "状态读取失败",
|
|
1690
2436
|
detail: "本机状态接口连续 " + connFailRef.v + " 次请求失败:"
|
|
1691
2437
|
+ ((e && e.message) ? e.message : String(e))
|
|
1692
|
-
+ "
|
|
1693
|
-
error: { code: "status_unreachable", message: "
|
|
2438
|
+
+ "(面板读不到后台服务状态,扫码/远程访问都不会推进)。可点「重试」,或刷新页面;若持续失败请把诊断信息发给客服。",
|
|
2439
|
+
error: { code: "status_unreachable", message: "无法读取本机后台服务状态(/dsh-remote/bridge-status 请求失败)" },
|
|
1694
2440
|
retryable: true,
|
|
1695
2441
|
attempts: connFailRef.v,
|
|
1696
2442
|
});
|
|
@@ -1714,12 +2460,15 @@ window.__ModuleLoader__.load({
|
|
|
1714
2460
|
var copyDiagnostics = function () {
|
|
1715
2461
|
var text = (connInfo && connInfo.diagnostics) || "";
|
|
1716
2462
|
if (!text) return;
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
2463
|
+
copyText(text).then(function (done) {
|
|
2464
|
+
if (!done) {
|
|
2465
|
+
// 剪贴板不可用:文案里已给出日志路径,用户可自行查看
|
|
2466
|
+
setMsg("warn", "复制失败:浏览器可能限制了剪贴板权限;可展开下方日志路径自行查看。");
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
setCopiedDiag(true);
|
|
2470
|
+
later(function () { setCopiedDiag(false); }, 2000);
|
|
2471
|
+
});
|
|
1723
2472
|
};
|
|
1724
2473
|
|
|
1725
2474
|
function setMsg(kind, text) { setMessage({ kind: kind, text: text }); }
|
|
@@ -1738,7 +2487,7 @@ window.__ModuleLoader__.load({
|
|
|
1738
2487
|
setSt(cfg);
|
|
1739
2488
|
var br = cfg && cfg.bridgeRestart;
|
|
1740
2489
|
if (br && br.ok === false && br.status !== "running" && br.status !== "provisioning" && br.status !== "skipped") {
|
|
1741
|
-
setMsg("warn", okText + "
|
|
2490
|
+
setMsg("warn", okText + ";但后台服务未能自动重启:"
|
|
1742
2491
|
+ ((br.detail || br.status) || "未知原因")
|
|
1743
2492
|
+ " —— 新账号可能收不到这台设备,请点下方「重启服务」或重启一次 dsh web。");
|
|
1744
2493
|
return;
|
|
@@ -1944,7 +2693,7 @@ window.__ModuleLoader__.load({
|
|
|
1944
2693
|
post(start ? "/dsh-remote/start" : "/dsh-remote/stop")
|
|
1945
2694
|
.then(function (body) {
|
|
1946
2695
|
setSt(body);
|
|
1947
|
-
setMsg(body.ok ? "ok" : "err", body.ok ? (start ? "✅
|
|
2696
|
+
setMsg(body.ok ? "ok" : "err", body.ok ? (start ? "✅ 后台服务已启动" + (body.pid ? " (pid=" + body.pid + ")" : "") : "后台服务已停止") : (body.detail || body.status || "操作失败"));
|
|
1948
2697
|
})
|
|
1949
2698
|
.catch(function (e) { setMsg("err", (start ? "启动" : "停止") + "失败: " + e.message); })
|
|
1950
2699
|
.finally(function () { setBusy(""); });
|
|
@@ -2011,23 +2760,36 @@ window.__ModuleLoader__.load({
|
|
|
2011
2760
|
fbClearThreads();
|
|
2012
2761
|
setPwdOpen(false);
|
|
2013
2762
|
setSt(body); setSelfHost(""); setLocalKey("");
|
|
2014
|
-
setMsg(body.ok ? "ok" : "err", body.ok ? "✅
|
|
2763
|
+
setMsg(body.ok ? "ok" : "err", body.ok ? "✅ 已切换到自建服务,后台服务已重启" : (body.error || (body.body && body.body.error) || "保存失败"));
|
|
2015
2764
|
})
|
|
2016
2765
|
.catch(function (e) { setMsg("err", "保存失败: " + e.message); })
|
|
2017
2766
|
.finally(function () { setBusy(""); });
|
|
2018
2767
|
};
|
|
2019
2768
|
|
|
2769
|
+
/**
|
|
2770
|
+
* 拉取邀请视图数据:公共配置(邀请规则/试用天数)+ 我的邀请记录。
|
|
2771
|
+
* 【0.6.9 修正】记录拉取失败**不再静默当成"没有记录"**:否则用户会以为
|
|
2772
|
+
* "新用户明明装好了却没记上"。失败时记下原因,由列表位置给出「重试」。
|
|
2773
|
+
*/
|
|
2020
2774
|
var loadInvite = function () {
|
|
2021
|
-
|
|
2775
|
+
setInviteLoading(true);
|
|
2776
|
+
setInviteErr(null);
|
|
2777
|
+
setInviteCopyErr(null);
|
|
2022
2778
|
api("/dsh-remote/remote-url").then(function (b) {
|
|
2023
2779
|
var pubBody = (b && b.publicConfig) || {};
|
|
2024
2780
|
setPub(pubBody);
|
|
2025
|
-
}).catch(function () {});
|
|
2781
|
+
}).catch(function () { /* 规则取不到 → 用默认 n=3/days=15 兜底,不打扰 */ });
|
|
2026
2782
|
api("/dsh-remote/invite-records").then(function (b) {
|
|
2027
|
-
if (b && b.ok)
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2783
|
+
if (b && b.ok) {
|
|
2784
|
+
setInviteData({ records: Array.isArray(b.records) ? b.records : [], rewards: Array.isArray(b.rewards) ? b.rewards : [] });
|
|
2785
|
+
} else {
|
|
2786
|
+
setInviteData({ records: [], rewards: [] });
|
|
2787
|
+
setInviteErr(String((b && (b.error || b.detail)) || "服务端未返回邀请记录"));
|
|
2788
|
+
}
|
|
2789
|
+
}).catch(function (e) {
|
|
2790
|
+
setInviteData({ records: [], rewards: [] });
|
|
2791
|
+
setInviteErr((e && e.message) ? String(e.message) : "网络错误");
|
|
2792
|
+
}).finally(function () { setInviteLoading(false); });
|
|
2031
2793
|
};
|
|
2032
2794
|
|
|
2033
2795
|
var field = function (label, inputEl) {
|
|
@@ -2035,6 +2797,210 @@ window.__ModuleLoader__.load({
|
|
|
2035
2797
|
};
|
|
2036
2798
|
var input = function (attrs) { return h("input", Object.assign({ className: "dru-input", type: "text" }, attrs)); };
|
|
2037
2799
|
var card = function (title, children) { return h("div", { className: "dru-card" }, title ? h("h3", null, title) : null, children); };
|
|
2800
|
+
/**
|
|
2801
|
+
* 邀请奖励口径:优先 public-config 下发的 invite_rule,取不到才用与线上一致的兜底常量。
|
|
2802
|
+
* off=true(n / days 被显式配成 0)表示拉新已关闭 → 所有拉新入口/卡片整体隐藏。
|
|
2803
|
+
*/
|
|
2804
|
+
function inviteRuleOf() {
|
|
2805
|
+
var raw = (pub && pub.invite_rule) || null;
|
|
2806
|
+
var r = raw || {};
|
|
2807
|
+
// 与 planFactsOf 同一套取值规则:**只判「缺没缺」,不判「大不大」**。
|
|
2808
|
+
// n / days 配成 0 是「关闭」这一合法状态(下面 off 会置 true 并隐藏所有拉新入口),
|
|
2809
|
+
// 绝不能用 `> 0` 把 0 吃掉再回落成 1/3 —— 那正是后端 `|| 3 / || 15` 的同款 bug。
|
|
2810
|
+
var n = firstNum([r.n]);
|
|
2811
|
+
var days = firstNum([r.days]);
|
|
2812
|
+
return {
|
|
2813
|
+
n: n === null ? INVITE_RULE_FALLBACK.n : n,
|
|
2814
|
+
days: days === null ? INVITE_RULE_FALLBACK.days : days,
|
|
2815
|
+
off: inviteOffFrom(raw)
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
/**
|
|
2819
|
+
* 🎁 邀请收益引导气泡(0.6.9)。
|
|
2820
|
+
*
|
|
2821
|
+
* 要解决的问题(用户原话):按钮只写了「带新用户换会员」四个字,**没有说清能拿到什么**
|
|
2822
|
+
* —— 用户看不出点进去是「带 1 位新用户 → 得 3 天 PRO」。所以把收益摆到按钮旁边。
|
|
2823
|
+
*
|
|
2824
|
+
* 数字**全部来自后台** public-config 的 invite_rule(走 inviteRuleOf):
|
|
2825
|
+
* · 关闭态(n / days 被配成 0)→ **不渲染**(活动都关了,再引导就是骗点击);
|
|
2826
|
+
* · 取不到配置 → inviteRuleOf 已回落成与线上一致的 {n:1, days:3};
|
|
2827
|
+
* · `0` 是合法值,绝不会被当成「取不到」再回落(见 inviteOffFrom 的注释)。
|
|
2828
|
+
* 同一屏只在这里出现一次 —— 账号卡是邀请的唯一入口,别处不再重复放。
|
|
2829
|
+
*
|
|
2830
|
+
* 交互克制:进入时一次 320ms 弹入,之后每 4.5s 一次轻微呼吸(占空比 ~28%,不是持续闪烁);
|
|
2831
|
+
* 可关闭且关闭后永久记住(localStorage);`prefers-reduced-motion: reduce` 下完全不动。
|
|
2832
|
+
* 无障碍:整条是 role=status + aria-live=polite + aria-atomic 的**完整句子**
|
|
2833
|
+
* (不播报裸数字);触发按钮用 aria-describedby 指过来,聚焦即能听到收益。
|
|
2834
|
+
*/
|
|
2835
|
+
function renderInviteTip() {
|
|
2836
|
+
if (inviteTipDismissed) return null;
|
|
2837
|
+
var r = inviteRuleOf();
|
|
2838
|
+
if (r.off) return null; // 活动关闭 → 不引导
|
|
2839
|
+
return h("div", { className: "dru-tip-wrap" },
|
|
2840
|
+
h("div", {
|
|
2841
|
+
className: "dru-tip",
|
|
2842
|
+
id: INVITE_TIP_ID,
|
|
2843
|
+
role: "status",
|
|
2844
|
+
"aria-live": "polite",
|
|
2845
|
+
"aria-atomic": "true"
|
|
2846
|
+
},
|
|
2847
|
+
h("span", { className: "dru-tip-ic", "aria-hidden": "true" }, "🎁"),
|
|
2848
|
+
h("span", null,
|
|
2849
|
+
"带 ", h("b", null, r.n + " 位新用户"), " → 你得 ", h("b", null, r.days + " 天 PRO")),
|
|
2850
|
+
h("button", {
|
|
2851
|
+
type: "button",
|
|
2852
|
+
className: "dru-tip-close",
|
|
2853
|
+
"aria-label": "关闭邀请奖励提示",
|
|
2854
|
+
title: "不再提示",
|
|
2855
|
+
onClick: function () { inviteTipDismiss(); setInviteTipDismissed(true); }
|
|
2856
|
+
}, "✕")
|
|
2857
|
+
)
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
/**
|
|
2861
|
+
* 把 public-config 的套餐配置收敛成一份事实对象(**线上真形状优先**)。
|
|
2862
|
+
*
|
|
2863
|
+
* 取值顺序(每个字段各自独立回落,绝不因为一个字段缺失就把整份配置丢掉):
|
|
2864
|
+
* mbps ← plans[key].max_mbps → plans[key].mbps(旧形状) → PLAN_FACTS[key].mbps
|
|
2865
|
+
* gb ← plans[key].monthly_gb → plans[key].gb(旧形状) → PLAN_FACTS[key].gb
|
|
2866
|
+
* price ← prices[key] → plans[key].price(旧形状) → PLAN_FACTS[key].price
|
|
2867
|
+
* `fromServer` = 服务端确实下发了本档的套餐对象(→ 数字已权威,不再写相对说法)。
|
|
2868
|
+
* name 只取本地常量:服务端不下发展示名,且它是 UI 文案不是配置。
|
|
2869
|
+
*/
|
|
2870
|
+
function planFactsOf(key) {
|
|
2871
|
+
var base = PLAN_FACTS[key] || PLAN_FACTS.pro;
|
|
2872
|
+
var srvPlan = pub && pub.plans ? pub.plans[key] : null;
|
|
2873
|
+
var srvPrices = pub && pub.prices ? pub.prices[key] : null;
|
|
2874
|
+
var obj = srvPlan && typeof srvPlan === "object" ? srvPlan : null;
|
|
2875
|
+
return {
|
|
2876
|
+
name: base.name,
|
|
2877
|
+
mbps: firstNum([obj && obj.max_mbps, obj && obj.mbps, base.mbps]),
|
|
2878
|
+
gb: firstNum([obj && obj.monthly_gb, obj && obj.gb, base.gb]),
|
|
2879
|
+
price: firstNum([srvPrices, obj && obj.price, base.price]),
|
|
2880
|
+
fromServer: !!obj
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
/** 免费档带宽(用于「免费档的 N 倍」):plans.free.max_mbps 优先,其次 quota.max_mbps,最后常量。 */
|
|
2884
|
+
function freeMbpsOf() {
|
|
2885
|
+
var srvFree = pub && pub.plans && pub.plans.free && typeof pub.plans.free === "object" ? pub.plans.free : null;
|
|
2886
|
+
return firstNum([srvFree && srvFree.max_mbps, srvFree && srvFree.mbps, quota && quota.max_mbps, FREE_FACTS.mbps]);
|
|
2887
|
+
}
|
|
2888
|
+
/**
|
|
2889
|
+
* 套餐规格文案(**先特性检测** pub.plans / pub.prices,取不到才走 PLAN_FACTS 常量)。
|
|
2890
|
+
* 支持形态:字符串(整段规格文案)|{ max_mbps, monthly_gb } + prices[key](线上真形状)
|
|
2891
|
+
* |{ mbps, gb, price }(旧形状,兼容保留)。
|
|
2892
|
+
* dimension="bandwidth" 时带带宽;服务端已下发本档数字 → **不再写**「免费档的 N 倍」相对说法
|
|
2893
|
+
* (相对说法只在常量兜底时才有意义,否则会和权威数字并用自相矛盾)。
|
|
2894
|
+
*/
|
|
2895
|
+
function planSpecText(key, dimension) {
|
|
2896
|
+
var srvStr = pub && pub.plans ? pub.plans[key] : null;
|
|
2897
|
+
if (typeof srvStr === "string" && srvStr) return srvStr;
|
|
2898
|
+
var f = planFactsOf(key);
|
|
2899
|
+
if (dimension !== "bandwidth") return f.gb + " GB/月,¥" + f.price + "/月";
|
|
2900
|
+
var spec = f.mbps + " Mbps";
|
|
2901
|
+
if (!f.fromServer) {
|
|
2902
|
+
var freeMbps = freeMbpsOf();
|
|
2903
|
+
var times = freeMbps && freeMbps > 0 ? Math.round(f.mbps / freeMbps) : 0;
|
|
2904
|
+
if (times > 1) spec += "(免费档的 " + times + " 倍)";
|
|
2905
|
+
}
|
|
2906
|
+
return spec + " · " + f.gb + " GB/月,¥" + f.price + "/月";
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* 转化那条路的套餐键与文案(dimension = "size" 流量维度 | "bandwidth" 带宽维度,
|
|
2910
|
+
* 后者用于「连接偏慢」场景)。免费 → 升 PRO;PRO → 升 Pro Max;Pro Max 无更高档 → null(只剩拉新路)。
|
|
2911
|
+
* label / title 同源(都走 planSpecText),服务端下发了 pub.plans 也不会自相矛盾。
|
|
2912
|
+
*/
|
|
2913
|
+
function upgradePath(dimension) {
|
|
2914
|
+
var plan = account ? account.plan : "free";
|
|
2915
|
+
var isMemberNow = plan === "pro" || plan === "pro_max";
|
|
2916
|
+
var key = !isMemberNow ? "pro" : plan === "pro" ? "pro_max" : "";
|
|
2917
|
+
if (!key) return null; // 已是 Pro Max:没有更高档,不显示「升级」这条
|
|
2918
|
+
var name = planFactsOf(key).name; // 展示名只来自本地常量(服务端不下发)
|
|
2919
|
+
var spec = planSpecText(key, dimension);
|
|
2920
|
+
return {
|
|
2921
|
+
key: key,
|
|
2922
|
+
label: "🚀 升级 " + name + ":" + spec,
|
|
2923
|
+
title: "升级到 " + name + ":" + spec + "(带登录态打开套餐页,会员流量按自然月计量)"
|
|
2924
|
+
};
|
|
2925
|
+
}
|
|
2926
|
+
/**
|
|
2927
|
+
* 🔀 转化 or 拉新(0.6.9):在用户**正在疼**的时刻(免费额度将尽 / 已被限速 / 连接偏慢)
|
|
2928
|
+
* 给一个明确二选一 —— 要么升级套餐(转化),要么带新用户换会员时长(拉新)。
|
|
2929
|
+
*
|
|
2930
|
+
* 与上一版的区别:两条路都是**真按钮**(.dru-btn,命中区 ≥44px、有 :focus-visible 焦点环),
|
|
2931
|
+
* 不再是一行 12px 灰字文字链;拉新那条必须说清是**交换**(带 N 位新用户 → 得 M 天 PRO),
|
|
2932
|
+
* 不能只说「邀请好友」。
|
|
2933
|
+
*
|
|
2934
|
+
* 事实口径(线上 public-config / 运营公布值,见文件顶部 PLAN_FACTS 注释):
|
|
2935
|
+
* · 邀请:每 n 位**新用户** → 邀请人得 days 天 PRO(invite_rule {n:1, days:3})
|
|
2936
|
+
* · 被邀请人**没有任何额外奖励**(trial_days = 0)→ 绝不写「双方都得」
|
|
2937
|
+
* · 计入条件:对方用你的链接**注册新账号** + 在电脑上**装好 dsh-remote-web 并上线**
|
|
2938
|
+
* · 会员按自然月计量:3 天是**会员时长**,不是流量(不写「3 天能拿 20GB」)
|
|
2939
|
+
* kind = "quota"(额度将尽/已限速)| "slow"(连接偏慢,带宽维度措辞)
|
|
2940
|
+
*
|
|
2941
|
+
* 关闭态(invite_rule.n / days 配成 0)与单路收敛:
|
|
2942
|
+
* · 拉新关闭 → 不渲染拉新按钮,也不留空位;文案改成单路(标题/副句/说明同步收口)。
|
|
2943
|
+
* · 已是 Pro Max(没有更高档)→ 只剩拉新那一路,此时它接主 CTA 样式。
|
|
2944
|
+
* · 两路都没了(Pro Max + 拉新关闭)→ **整块返回 null**,不留一个空壳块。
|
|
2945
|
+
*/
|
|
2946
|
+
function renderDualPath(kind) {
|
|
2947
|
+
var r = inviteRuleOf();
|
|
2948
|
+
var isSlow = kind === "slow";
|
|
2949
|
+
// 慢 → 带宽维度措辞(5 Mbps / 免费档的 5 倍);额度将尽 → 流量维度措辞(20 GB/月)
|
|
2950
|
+
var up = upgradePath(isSlow ? "bandwidth" : "size");
|
|
2951
|
+
var inviteOk = !r.off;
|
|
2952
|
+
if (!up && !inviteOk) return null; // 两路都没有 → 整块不显示(会员 + 拉新关闭)
|
|
2953
|
+
var twoWay = !!up && inviteOk;
|
|
2954
|
+
var inviteLabel = "🎁 带 " + r.n + " 位新用户:换 " + r.days + " 天 PRO";
|
|
2955
|
+
var inviteTitle = "带 " + r.n + " 位还没注册过的新用户(用你的链接注册 + 装好电脑端并上线)→ 你得 " + r.days + " 天 PRO";
|
|
2956
|
+
var title = twoWay
|
|
2957
|
+
? (isSlow ? "⚡ 连接偏慢,两条路提升带宽" : "⚡ 免费额度将尽 / 已被限速,两条路接着用")
|
|
2958
|
+
: up
|
|
2959
|
+
? (isSlow ? "⚡ 连接偏慢:升级带宽可以更快" : "⚡ 免费额度将尽 / 已被限速:升级套餐继续用")
|
|
2960
|
+
: (isSlow ? "⚡ 连接偏慢:带新用户可换会员时长" : "⚡ 免费额度将尽 / 已被限速:带新用户可换会员时长");
|
|
2961
|
+
var sub = twoWay
|
|
2962
|
+
? (isSlow
|
|
2963
|
+
? "免费档带宽有限:排队、跨网、高峰期会被限速,这是慢的主因。要么升级带宽,要么带新用户换会员时长。"
|
|
2964
|
+
: "免费额度用完会被限速,继续用只有两条路:要么升级套餐,要么带一位新用户换会员时长。")
|
|
2965
|
+
: up
|
|
2966
|
+
? (isSlow
|
|
2967
|
+
? "免费档带宽有限:排队、跨网、高峰期会被限速,这是慢的主因;升级带宽可直接改善。"
|
|
2968
|
+
: "免费额度用完会被限速;升级套餐即可继续用(会员流量按自然月计量)。")
|
|
2969
|
+
: "带一位还没注册过的新用户,换 " + r.days + " 天 PRO:对方用你的邀请链接注册新账号 + 在电脑上装好 dsh-remote-web 并上线后自动到账。";
|
|
2970
|
+
return h("div", { className: "dru-dual" },
|
|
2971
|
+
h("div", { className: "dru-dual-title" }, title),
|
|
2972
|
+
h("div", { className: "dru-dual-sub" }, sub),
|
|
2973
|
+
h("div", { className: "dru-dual-actions" },
|
|
2974
|
+
// 转化路:带登录态打开套餐页,复用既有 openUpgradeAuth,不凭空造 URL
|
|
2975
|
+
up
|
|
2976
|
+
? h("button", {
|
|
2977
|
+
type: "button",
|
|
2978
|
+
className: "dru-btn dru-btn-primary",
|
|
2979
|
+
disabled: busy !== "",
|
|
2980
|
+
title: up.title,
|
|
2981
|
+
onClick: openUpgradeAuth
|
|
2982
|
+
}, busy === "upgrade" ? "生成链接中…" : up.label)
|
|
2983
|
+
: null,
|
|
2984
|
+
// 拉新路(关闭态整体不渲染):必须能点,且必须是交换句式;只剩它一路时接主 CTA 样式
|
|
2985
|
+
inviteOk
|
|
2986
|
+
? h("button", {
|
|
2987
|
+
type: "button",
|
|
2988
|
+
className: "dru-btn " + (up ? "dru-btn-ghost" : "dru-btn-primary"),
|
|
2989
|
+
disabled: busy !== "",
|
|
2990
|
+
title: inviteTitle,
|
|
2991
|
+
onClick: function () { setView("invite"); loadInvite(); }
|
|
2992
|
+
}, inviteLabel)
|
|
2993
|
+
: null),
|
|
2994
|
+
// 交换说明只在拉新可用时出现(关闭态或纯升级态不该再讲拉新)
|
|
2995
|
+
inviteOk
|
|
2996
|
+
? h("div", { className: "dru-dual-note" },
|
|
2997
|
+
"拉新这条路是", h("b", null, "交换"), ":对方", h("b", null, "用你的邀请链接注册新账号"), ",并在电脑上",
|
|
2998
|
+
h("b", null, "装好 dsh-remote-web 并上线"), "后,", h("b", null, r.days + " 天 PRO 自动到账"),
|
|
2999
|
+
";只有你得奖,对方没有额外奖励(对方必须是还没注册过的新用户才计入)。",
|
|
3000
|
+
isSlow ? "会员按自然月计量," + r.days + " 天是会员时长、不是流量。" : "")
|
|
3001
|
+
: null
|
|
3002
|
+
);
|
|
3003
|
+
}
|
|
2038
3004
|
|
|
2039
3005
|
// ---------- 我的信息(个人中心) ----------
|
|
2040
3006
|
function fmtDate(ts) {
|
|
@@ -2066,6 +3032,21 @@ window.__ModuleLoader__.load({
|
|
|
2066
3032
|
else if (source === "trial") planText = "试用 PRO 会员 · 到期 " + fmtDate(a.trial_expires_at);
|
|
2067
3033
|
else if (endsAt) planText = plan === "pro_max" ? "Pro Max 会员 · 到期 " + fmtDate(endsAt) : "PRO 会员 · 到期 " + fmtDate(endsAt);
|
|
2068
3034
|
else planText = plan === "pro_max" ? "Pro Max 长期会员" : "PRO 长期会员";
|
|
3035
|
+
// 【0.6.9】「额度将尽 / 已被限速」判定:只依据服务端已下发的额度字段
|
|
3036
|
+
// · limit_enabled 且 percent ≥ 80(接近用尽)
|
|
3037
|
+
// · limit_enabled 但没下发 max_mbps(文案已显示「已限速」=正在被限速)
|
|
3038
|
+
// 判断不了(quota 缺失、不限量、会员)→ 不显示,绝不误报。
|
|
3039
|
+
// 注意:会员(PRO / Pro Max)的额度不吃这条免费档判断,所以「升级」永远不会弹给付费用户,
|
|
3040
|
+
// 但连接偏慢那条(renderConnectBlock)对会员同样成立 —— 会员也会遇到慢,那时只剩拉新路。
|
|
3041
|
+
var quotaPctNum = Number(quotaPct);
|
|
3042
|
+
var quotaTight = !!(quota && quota.limit_enabled && !isMember &&
|
|
3043
|
+
((isFinite(quotaPctNum) && quotaPctNum >= 80) || !quota.max_mbps));
|
|
3044
|
+
// 拉新关闭态(invite_rule.n / days = 0)→ 账号卡入口整体隐藏,不留点进去没内容的入口
|
|
3045
|
+
var invOff = inviteRuleOf().off;
|
|
3046
|
+
// 🎁 引导气泡是否在屏:活动没关 + 用户没关掉过。
|
|
3047
|
+
// 计算一次给按钮的 aria-describedby 用(气泡没渲染时不能指向不存在的 id)。
|
|
3048
|
+
var invTipOn = !invOff && !inviteTipDismissed;
|
|
3049
|
+
var invTipRule = invTipOn ? inviteRuleOf() : null;
|
|
2069
3050
|
return h("div", null,
|
|
2070
3051
|
h("div", { className: "dru-user" },
|
|
2071
3052
|
h("div", { className: "dru-avatar" }, (st.config.phone || "D").charAt(0).toUpperCase()),
|
|
@@ -2082,14 +3063,27 @@ window.__ModuleLoader__.load({
|
|
|
2082
3063
|
h("div", { className: "dru-actions", style: { marginTop: 10 } },
|
|
2083
3064
|
h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { display: "inline-flex", alignItems: "center" }, disabled: busy !== "", title: "升级/续费(带登录态打开)", onClick: openUpgradeAuth },
|
|
2084
3065
|
busy === "upgrade" ? "生成链接中…" : (!isMember ? "🚀 升级 PRO" : source === "trial" ? "🚀 转正式 PRO" : "🔄 续费会员")),
|
|
2085
|
-
|
|
3066
|
+
// 拉新关闭 → 入口整体隐藏(否则会点进一个没有内容的邀请页)
|
|
3067
|
+
// 按钮文字自带可访问名称;气泡在屏时用 aria-describedby 把「能得多少」也读出来。
|
|
3068
|
+
invOff ? null : h("button", {
|
|
3069
|
+
type: "button",
|
|
3070
|
+
className: "dru-btn dru-btn-ghost",
|
|
3071
|
+
disabled: busy !== "",
|
|
3072
|
+
title: invTipRule ? "带 " + invTipRule.n + " 位新用户可得 " + invTipRule.days + " 天 PRO 会员" : "带新用户换会员",
|
|
3073
|
+
"aria-describedby": invTipOn ? INVITE_TIP_ID : undefined,
|
|
3074
|
+
onClick: function () { setView("invite"); loadInvite(); }
|
|
3075
|
+
}, "🎁 带新用户换会员"),
|
|
2086
3076
|
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("feedback"); } }, "💬 用户反馈"),
|
|
2087
3077
|
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: togglePwdForm }, pwdOpen ? "收起修改密码" : "🔒 修改密码"),
|
|
2088
3078
|
h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { doLogout(false); } }, "退出登录")
|
|
2089
3079
|
),
|
|
3080
|
+
// 🎁 邀请收益引导气泡:紧贴上面那排按钮(不遮挡按钮、窄屏独占一行)
|
|
3081
|
+
renderInviteTip(),
|
|
2090
3082
|
h("div", { className: "dru-hint", style: { marginTop: 8 } },
|
|
2091
3083
|
"升级/续费以带登录态方式打开:点击后生成一次性访问链接并直接跳转,无需重新登录。" +
|
|
2092
3084
|
(endsAt && isMember ? "到期后如需继续使用会员权益,请在到期前续费。" : "")),
|
|
3085
|
+
// 额度将尽 / 已被限速 → 🔀 双路块:升级套餐(转化)or 带新用户换会员时长(拉新)
|
|
3086
|
+
quotaTight ? renderDualPath("quota") : null,
|
|
2093
3087
|
pwdOpen ? renderResetPwdForm(false) : null
|
|
2094
3088
|
);
|
|
2095
3089
|
}
|
|
@@ -2102,7 +3096,7 @@ window.__ModuleLoader__.load({
|
|
|
2102
3096
|
// 浏览器端不再出现明文手机号。登录卡「忘记密码」为用户自行输入,原样使用。
|
|
2103
3097
|
var curPhone = ((st && st.config && st.config.phone) || "");
|
|
2104
3098
|
var ph = fromLogin ? phone : "";
|
|
2105
|
-
return h("div", { className: "dru-card", style: { marginTop: 10, border: "1px dashed
|
|
3099
|
+
return h("div", { className: "dru-card", style: { marginTop: 10, border: "1px dashed var(--dru-border)" } },
|
|
2106
3100
|
h("h3", null, fromLogin ? "忘记密码(短信重置)" : "🔒 修改密码(短信验证)"),
|
|
2107
3101
|
h("div", { className: "dru-hint", style: { marginBottom: 8 } },
|
|
2108
3102
|
fromLogin
|
|
@@ -2134,46 +3128,285 @@ window.__ModuleLoader__.load({
|
|
|
2134
3128
|
);
|
|
2135
3129
|
}
|
|
2136
3130
|
|
|
2137
|
-
// ----------
|
|
3131
|
+
// ---------- 🎁 邀请视图(0.6.9 重排:交换大字 → 三步走 → 进度 → 邀请码/链接一键复制 → 折叠规则 → 记录) ----------
|
|
3132
|
+
/** 体面空态:图标 + 标题 + 说明 + 可选操作(替代过去"一行灰字了事")。 */
|
|
3133
|
+
function emptyState(icon, title, sub, actions) {
|
|
3134
|
+
return h("div", { className: "dru-empty" },
|
|
3135
|
+
h("div", { className: "dru-empty-icon", "aria-hidden": "true" }, icon),
|
|
3136
|
+
h("div", { className: "dru-empty-title" }, title),
|
|
3137
|
+
sub ? h("div", { className: "dru-empty-sub" }, sub) : null,
|
|
3138
|
+
actions && actions.length ? h("div", { className: "dru-empty-actions" }, actions) : null
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
/**
|
|
3142
|
+
* 一键复制按钮:成功后按钮自身变绿并显示「已复制」2 秒;失败则在按钮旁(同一个 .dru-copy-row)
|
|
3143
|
+
* 给出红字提示 —— 错误靠近出错位置,不让用户以为"点了没反应"。
|
|
3144
|
+
*/
|
|
3145
|
+
function copyButton(key, text, label) {
|
|
3146
|
+
var ok = inviteCopied === key;
|
|
3147
|
+
return h("button", {
|
|
3148
|
+
type: "button",
|
|
3149
|
+
className: "dru-copy-btn" + (ok ? " copied" : ""),
|
|
3150
|
+
disabled: !text,
|
|
3151
|
+
"aria-label": label,
|
|
3152
|
+
onClick: function () {
|
|
3153
|
+
copyText(text).then(function (done) {
|
|
3154
|
+
if (!done) {
|
|
3155
|
+
setInviteCopied("");
|
|
3156
|
+
setInviteCopyErr({ kind: key, text: "复制失败:请手动选中上面的文字复制(浏览器可能限制了剪贴板权限)" });
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
setInviteCopyErr(null);
|
|
3160
|
+
setInviteCopied(key);
|
|
3161
|
+
later(function () { setInviteCopied(function (cur) { return cur === key ? "" : cur; }); }, 2000);
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
}, ok ? "✅ 已复制" : label);
|
|
3165
|
+
}
|
|
3166
|
+
/** 邀请码 / 邀请链接的一行(标签 + 值 + 复制按钮 + 就近错误提示)。 */
|
|
3167
|
+
function copyRow(label, value, kind, hint) {
|
|
3168
|
+
var isCode = kind === "code";
|
|
3169
|
+
return h("div", { className: "dru-copy-row" },
|
|
3170
|
+
h("div", { className: "dru-copy-label" }, label),
|
|
3171
|
+
h("div", { className: "dru-copy-box" },
|
|
3172
|
+
h("span", { className: isCode ? "dru-code-val" : "dru-link-val" }, value),
|
|
3173
|
+
copyButton(kind, value, isCode ? "复制邀请码" : "复制邀请链接")
|
|
3174
|
+
),
|
|
3175
|
+
hint ? h("div", { className: "dru-invite-sub", style: { marginTop: 6 } }, hint) : null,
|
|
3176
|
+
// 复制失败提示紧贴这一次操作的行(错误靠近出错位置)
|
|
3177
|
+
inviteCopyErr && inviteCopyErr.kind === kind
|
|
3178
|
+
? h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 6 } }, inviteCopyErr.text)
|
|
3179
|
+
: null
|
|
3180
|
+
);
|
|
3181
|
+
}
|
|
3182
|
+
/** 邀请记录里的新用户展示名:只用接口给的 invitee_phone(掩码),缺字段时降级,不编造。 */
|
|
3183
|
+
function inviteeName(r) {
|
|
3184
|
+
var phone = maskPhoneLike(r && r.invitee_phone);
|
|
3185
|
+
if (phone) return phone;
|
|
3186
|
+
var dev = r && r.device_id ? String(r.device_id) : "";
|
|
3187
|
+
if (dev) return "新用户(设备 " + (dev.length > 6 ? dev.slice(-6) : dev) + ")";
|
|
3188
|
+
return "新用户";
|
|
3189
|
+
}
|
|
3190
|
+
/** 一条邀请记录:谁 / 什么时候 / 是否已生效(时间或状态字段缺失时如实标注,不编造)。 */
|
|
3191
|
+
function renderInviteRecords(records, ruleN) {
|
|
3192
|
+
return h("div", null, records.map(function (r, i) {
|
|
3193
|
+
var ts = r && (r.created_at || r.bound_at || r.at);
|
|
3194
|
+
// 记录来自「有效邀请」接口:默认即已生效;若服务端给了 reward_granted:false → 标"待发放"
|
|
3195
|
+
var pending = r && r.reward_granted === false;
|
|
3196
|
+
return h("div", { key: (r && r.id != null ? String(r.id) : "rec-" + i), className: "dru-rec" },
|
|
3197
|
+
h("div", { className: "dru-rec-top" },
|
|
3198
|
+
h("span", { className: "dru-rec-who" }, inviteeName(r)),
|
|
3199
|
+
h("span", { className: "dru-rec-tag " + (pending ? "dru-rec-tag-wait" : "dru-rec-tag-ok") },
|
|
3200
|
+
pending ? "待发放奖励" : "已生效"),
|
|
3201
|
+
h("span", { className: "dru-rec-when" }, ts ? fmtDate(ts) : "时间未知")
|
|
3202
|
+
),
|
|
3203
|
+
h("div", { className: "dru-rec-sub" },
|
|
3204
|
+
pending
|
|
3205
|
+
? (ruleN > 0
|
|
3206
|
+
? "这位新用户已完成设备绑定,奖励按规则结算(每满 " + ruleN + " 位发放一次)。"
|
|
3207
|
+
// 拉新关闭态:不引用具体结算规则(不能显示「每满 0 位」这种假承诺)
|
|
3208
|
+
: "这位新用户已完成设备绑定;奖励结算以运营规则为准(活动当前未开放)。")
|
|
3209
|
+
: "这位新用户已完成电脑端安装并上线,已计入你的有效邀请。")
|
|
3210
|
+
);
|
|
3211
|
+
}));
|
|
3212
|
+
}
|
|
3213
|
+
/** 已到账奖励:每次奖励 = rule_days 天 PRO(时间字段缺失时只显示天数)。 */
|
|
3214
|
+
function renderInviteRewards(rewards, ruleDays) {
|
|
3215
|
+
if (!rewards.length) return null;
|
|
3216
|
+
return h("div", { className: "dru-copy-row" },
|
|
3217
|
+
h("div", { className: "dru-copy-label" }, "已到账奖励(" + rewards.length + " 次)"),
|
|
3218
|
+
h("div", null, rewards.map(function (r, i) {
|
|
3219
|
+
var days = r && (r.rule_days != null ? r.rule_days : ruleDays);
|
|
3220
|
+
var ts = r && (r.created_at || r.at);
|
|
3221
|
+
return h("div", { key: (r && r.id != null ? String(r.id) : "rw-" + i), className: "dru-rec" },
|
|
3222
|
+
h("div", { className: "dru-rec-top" },
|
|
3223
|
+
h("span", { className: "dru-rec-who" }, "+" + (days != null ? days : ruleDays) + " 天 PRO 会员"),
|
|
3224
|
+
h("span", { className: "dru-rec-tag dru-rec-tag-ok" }, "已到账"),
|
|
3225
|
+
h("span", { className: "dru-rec-when" }, ts ? fmtDate(ts) : "时间未提供")
|
|
3226
|
+
)
|
|
3227
|
+
);
|
|
3228
|
+
}))
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
/**
|
|
3232
|
+
* 「我的邀请记录」卡:邀请视图与**拉新关闭态**共用。
|
|
3233
|
+
* 关闭态下这是唯一保留的卡片 —— 记录是历史事实(不承诺任何未来奖励),
|
|
3234
|
+
* 关上活动不该让用户看不到自己已经计入的有效邀请与已到账奖励。
|
|
3235
|
+
*/
|
|
3236
|
+
function renderInviteRecordCard(records, rewards, validCount, ruleN, ruleDays, inviteErr, inviteLoading) {
|
|
3237
|
+
return card("我的邀请记录" + (inviteErr ? "" : "(" + validCount + ")"), [
|
|
3238
|
+
inviteLoading
|
|
3239
|
+
? h("div", { className: "dru-invite-sub" }, "正在读取邀请记录…")
|
|
3240
|
+
: inviteErr
|
|
3241
|
+
? h("div", null,
|
|
3242
|
+
h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 0 } }, "读取邀请记录失败:" + inviteErr),
|
|
3243
|
+
h("div", { className: "dru-actions", style: { marginTop: 8 } },
|
|
3244
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: inviteLoading, onClick: loadInvite }, "重试")))
|
|
3245
|
+
: validCount === 0
|
|
3246
|
+
? emptyState("📨", "还没有新用户通过你的链接进来",
|
|
3247
|
+
"把上面的邀请链接发给一位还没注册过的新用户:对方注册 + 在电脑上装好 dsh-remote-web 并上线后,会自动出现在这里。")
|
|
3248
|
+
: h("div", null, [
|
|
3249
|
+
h("div", { className: "dru-rec-head" },
|
|
3250
|
+
h("span", { style: { flex: "1" } }, "新用户"),
|
|
3251
|
+
h("span", null, "状态"),
|
|
3252
|
+
h("span", null, "时间")),
|
|
3253
|
+
renderInviteRecords(records, ruleN),
|
|
3254
|
+
renderInviteRewards(rewards, ruleDays),
|
|
3255
|
+
h("div", { className: "dru-invite-sub", style: { marginTop: 6 } },
|
|
3256
|
+
"名单里的新用户都已完成电脑端安装并上线,所以都算有效邀请。")
|
|
3257
|
+
])
|
|
3258
|
+
]);
|
|
3259
|
+
}
|
|
3260
|
+
/** 活动规则:默认折叠,点开才显示(层级清晰,不再和奖励说明挤在一堆小字里)。 */
|
|
3261
|
+
function renderInviteRules(n, days) {
|
|
3262
|
+
return h("div", { className: "dru-card" },
|
|
3263
|
+
h("button", {
|
|
3264
|
+
type: "button",
|
|
3265
|
+
className: "dru-disclose",
|
|
3266
|
+
"aria-expanded": inviteRulesOpen ? "true" : "false",
|
|
3267
|
+
onClick: function () { setInviteRulesOpen(!inviteRulesOpen); }
|
|
3268
|
+
},
|
|
3269
|
+
h("span", null, "活动规则"),
|
|
3270
|
+
h("span", { className: "dru-disclose-caret" }, inviteRulesOpen ? "收起 ▲" : "展开 ▼")
|
|
3271
|
+
),
|
|
3272
|
+
inviteRulesOpen
|
|
3273
|
+
? h("ol", { className: "dru-rules" }, [
|
|
3274
|
+
h("li", null, "这是", h("b", null, "交换"), ":你带新用户,平台给你会员时长 —— 每带 ", h("b", null, n + " 位新用户"), ",你得 ", h("b", null, days + " 天 PRO"), "。"),
|
|
3275
|
+
h("li", null, h("b", null, "只有新注册账号才计入"), ":对方必须是还没注册过的新用户,且通过你的邀请链接注册(或在注册时填写你的邀请码),才与你绑定邀请关系。"),
|
|
3276
|
+
h("li", null, "对方还要在这台电脑上", h("b", null, "装好 dsh-remote-web 并成功上线"), ",才计入 1 位", h("b", null, "有效邀请"), ";同一位新用户只计一次。"),
|
|
3277
|
+
h("li", null, h("b", null, "奖励只发给邀请人"), ":你得 ", h("b", null, days + " 天 PRO"), ",被邀请的新用户没有额外的邀请奖励(注册送礼与邀请无关)。"),
|
|
3278
|
+
h("li", null, "奖励自动到账,无需申请:可在「我的邀请记录」下方看到「已到账奖励」,在原到期时间上顺延(已是长期会员则保持长期)。"),
|
|
3279
|
+
h("li", null, h("b", null, days + " 天是会员时长、不是流量"), ":会员流量按自然月计量,天数是按天顺延的会员有效期。"),
|
|
3280
|
+
h("li", null, "邀请码与邀请链接长期有效,随时可在本页复制;新用户解绑或重装设备不会撤回已计入的邀请。"),
|
|
3281
|
+
h("li", null, "规则与数值以本页面显示的为准(运营可能调整活动力度)。")
|
|
3282
|
+
])
|
|
3283
|
+
: null
|
|
3284
|
+
);
|
|
3285
|
+
}
|
|
3286
|
+
|
|
2138
3287
|
function renderInvite() {
|
|
2139
|
-
var
|
|
3288
|
+
var inv = inviteRuleOf();
|
|
3289
|
+
var ruleN = inv.n;
|
|
3290
|
+
var ruleDays = inv.days;
|
|
3291
|
+
var trialDays = Number(pub && pub.trial_days) > 0 ? Number(pub.trial_days) : 0; // 注册送礼(关闭时不下发/为 0)
|
|
2140
3292
|
var code = (account && account.invite_code) || "";
|
|
2141
3293
|
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
|
|
2144
|
-
|
|
3294
|
+
var link = code ? base.replace(/\/+$/, "") + "/?invite=" + encodeURIComponent(code) : "";
|
|
3295
|
+
var loggedInSaaS = !!(st && st.config && st.config.phone);
|
|
3296
|
+
var records = (inviteData && inviteData.records) || [];
|
|
3297
|
+
var rewards = (inviteData && inviteData.rewards) || [];
|
|
3298
|
+
var validCount = records.length;
|
|
3299
|
+
// 进度:每 ruleN 位结算一次(与后端 onDeviceBound 的 floor(total/n) 语义一致)
|
|
3300
|
+
var toward = ruleN > 0 ? validCount % ruleN : 0;
|
|
3301
|
+
var nextGap = toward === 0 ? ruleN : ruleN - toward;
|
|
3302
|
+
var pct = ruleN > 0 ? Math.round((toward / ruleN) * 100) : 0;
|
|
3303
|
+
var copyLink = function () {
|
|
3304
|
+
copyText(link).then(function (done) {
|
|
3305
|
+
if (!done) { setInviteCopied(""); setInviteCopyErr("复制失败:请长按/手动选中上面的链接复制"); return; }
|
|
3306
|
+
setInviteCopyErr(null);
|
|
3307
|
+
setInviteCopied("link");
|
|
3308
|
+
later(function () { setInviteCopied(""); }, 2000);
|
|
3309
|
+
});
|
|
2145
3310
|
};
|
|
3311
|
+
|
|
3312
|
+
// ── 拉新关闭态(invite_rule.n / days 配成 0)──────────────────────────────
|
|
3313
|
+
// 邀请卡**整卡不渲染**:不显示「带 0 位新用户 → 0 天 PRO」,也不留只有一个复制按钮的空壳;
|
|
3314
|
+
// 邀请码/邀请链接(拉新的工具)与活动规则(引用 n/days)一并收起。
|
|
3315
|
+
// 只保留「我的邀请记录」——那是**历史事实**,不是承诺,且关闭活动不该让用户看不到自己已计入的记录。
|
|
3316
|
+
// 这一页在关闭态下正常也进不来(所有入口都隐藏了),此处是防御性渲染(例如停留在本页时配置刷成关闭)。
|
|
3317
|
+
if (inv.off) {
|
|
3318
|
+
return h("div", null,
|
|
3319
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { width: "100%" }, onClick: function () { setView("home"); } }, "← 返回账号"),
|
|
3320
|
+
card("🎁 带新用户换会员时长 · 当前未开放", [
|
|
3321
|
+
h("div", { className: "dru-invite-sub" },
|
|
3322
|
+
"运营已暂时关闭「带新用户换会员时长」活动,所以这里不再显示邀请链接与奖励进度。"),
|
|
3323
|
+
h("div", { className: "dru-invite-sub", style: { marginTop: 8 } },
|
|
3324
|
+
"你已有的历史记录不受影响(下方「我的邀请记录」仍可查看);重新开放后本页会自动恢复。")
|
|
3325
|
+
]),
|
|
3326
|
+
loggedInSaaS
|
|
3327
|
+
? renderInviteRecordCard(records, rewards, validCount, 0, ruleDays, inviteErr, inviteLoading)
|
|
3328
|
+
: null,
|
|
3329
|
+
message ? h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text) : null
|
|
3330
|
+
);
|
|
3331
|
+
}
|
|
3332
|
+
|
|
2146
3333
|
return h("div", null,
|
|
2147
|
-
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { width: "100%" }, onClick: function () { setView("home"); } }, "←
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
h("
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
h("
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
3334
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { width: "100%" }, onClick: function () { setView("home"); } }, "← 返回账号"),
|
|
3335
|
+
|
|
3336
|
+
// ① 交换是什么(大字:带 N 位新用户 → 得 M 天 PRO;一句人话说清条件与「只有你得奖」)
|
|
3337
|
+
card("🎁 带新用户,换会员时长", [
|
|
3338
|
+
h("div", { className: "dru-invite-hero" },
|
|
3339
|
+
"带 ", h("em", null, ruleN + " 位"), "新用户 → 得 ", h("em", null, ruleDays + " 天 PRO")),
|
|
3340
|
+
h("div", { className: "dru-invite-sub" },
|
|
3341
|
+
"这是一次交换:新用户", h("b", null, "用你的邀请链接注册新账号"), ",并在电脑上",
|
|
3342
|
+
h("b", null, "装好 dsh-remote-web 并上线"), "后,", ruleDays + " 天 PRO ",
|
|
3343
|
+
"自动到账(每满 " + ruleN + " 位结算一次,会员天数可累加);",
|
|
3344
|
+
h("b", null, "只有你得奖"),
|
|
3345
|
+
trialDays > 0
|
|
3346
|
+
? ",新用户自己注册即得 " + trialDays + " 天 PRO 试用,不用邀请也一样。"
|
|
3347
|
+
: ",新用户没有额外的邀请奖励(必须是还没注册过的新用户才计入)。"),
|
|
3348
|
+
// 三步走:把「怎么带」讲到不需要思考(① 复制 → ② 发给没注册过的新用户 → ③ 对方上线即到账)
|
|
3349
|
+
h("ol", { className: "dru-steps" }, [
|
|
3350
|
+
h("li", null, "复制", h("b", null, "你的专属邀请链接"), "(下方「我的邀请链接」,点一下就复制)。"),
|
|
3351
|
+
h("li", null, "发给一位", h("b", null, "还没注册过的新用户"), "(对方注册时自动带上你的邀请码)。"),
|
|
3352
|
+
h("li", null, "他注册新账号 + 在电脑上装好 dsh-remote-web 并上线 → ", h("b", null, ruleDays + " 天 PRO 自动到账"), "。")
|
|
3353
|
+
]),
|
|
3354
|
+
// 进度:已带来 / 已到账 / 还差几位(数据全部来自接口,未登录或未加载时不编造数字)
|
|
3355
|
+
loggedInSaaS
|
|
3356
|
+
? h("div", { className: "dru-invite-prog" },
|
|
3357
|
+
h("div", { className: "dru-invite-prog-top" },
|
|
3358
|
+
h("span", null, "已带来 ", h("span", { className: "n" }, String(validCount)), " 位新用户"),
|
|
3359
|
+
inviteErr ? null : h("span", null, "已到账 " + rewards.length + " 次(每次 " + ruleDays + " 天)")
|
|
3360
|
+
),
|
|
3361
|
+
h("div", { className: "dru-invite-bar", role: "img", "aria-label": "距离下一次奖励的进度" },
|
|
3362
|
+
h("div", { className: "dru-invite-bar-fill", style: { width: pct + "%" } })),
|
|
3363
|
+
h("div", { className: "dru-invite-next" },
|
|
3364
|
+
inviteLoading
|
|
3365
|
+
? "正在读取你的邀请记录…"
|
|
3366
|
+
: "本轮已累计 " + toward + "/" + ruleN + " 位,再带 " + nextGap + " 位新用户即可获得下一次 " + ruleDays + " 天 PRO。")
|
|
3367
|
+
)
|
|
3368
|
+
: null,
|
|
3369
|
+
// ② 我的邀请码 / 邀请链接(一键复制)
|
|
3370
|
+
code
|
|
3371
|
+
? h("div", null,
|
|
3372
|
+
copyRow("我的邀请码", code, "code", "新用户注册时填这个码,或直接用下面的邀请链接(自动带上)。"),
|
|
3373
|
+
copyRow("我的邀请链接", link, "link", "链接较长,可一键复制后发给还没注册过的新用户;对方打开即进入注册页。")
|
|
3374
|
+
)
|
|
3375
|
+
: loggedInSaaS
|
|
3376
|
+
? h("div", { style: { marginTop: 10 } },
|
|
3377
|
+
emptyState("🎫", "正在为你生成专属邀请码",
|
|
3378
|
+
"邀请码绑定你的手机号账号:新用户通过它注册后,奖励会自动记到这个账号上。(偶尔需要手动获取一次)",
|
|
3379
|
+
[h("button", {
|
|
3380
|
+
type: "button",
|
|
3381
|
+
className: "dru-btn dru-btn-primary",
|
|
3382
|
+
disabled: busy !== "",
|
|
3383
|
+
onClick: function () {
|
|
3384
|
+
setBusy("invite-refresh");
|
|
3385
|
+
api("/dsh-remote/account").then(function (b) {
|
|
3386
|
+
if (b && b.ok && b.account && b.account.invite_code) { setAccount(b.account); setMsg("ok", "邀请码已生成,可一键复制"); }
|
|
3387
|
+
else setMsg("err", "获取邀请码失败:请稍后重试(若仍未登录,请先在「🔑 账号」登录)");
|
|
3388
|
+
}).catch(function (e) { setMsg("err", "获取邀请码失败:" + e.message); })
|
|
3389
|
+
.finally(function () { setBusy(""); });
|
|
3390
|
+
}
|
|
3391
|
+
}, busy === "invite-refresh" ? "生成中…" : "获取我的邀请码")])
|
|
3392
|
+
)
|
|
3393
|
+
: h("div", { style: { marginTop: 10 } },
|
|
3394
|
+
emptyState("🔑", "登录后即可获得你的专属邀请码",
|
|
3395
|
+
"邀请码绑定你的手机号账号:新用户通过它注册后,奖励会自动记到这个账号上。登录后回到本页即可复制分享。",
|
|
3396
|
+
[h("button", { type: "button", className: "dru-btn dru-btn-primary", onClick: function () { setView("home"); } }, "去登录")])
|
|
3397
|
+
)
|
|
2171
3398
|
]),
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
3399
|
+
|
|
3400
|
+
// ③ 邀请记录(谁 / 什么时候 / 是否已生效)
|
|
3401
|
+
loggedInSaaS
|
|
3402
|
+
? renderInviteRecordCard(records, rewards, validCount, ruleN, ruleDays, inviteErr, inviteLoading)
|
|
3403
|
+
: null,
|
|
3404
|
+
|
|
3405
|
+
// ④ 活动规则(默认折叠)
|
|
3406
|
+
renderInviteRules(ruleN, ruleDays),
|
|
3407
|
+
|
|
3408
|
+
// 复制失败等就地提示见各 copyRow(错误靠近出错位置)
|
|
3409
|
+
message ? h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text) : null
|
|
2177
3410
|
);
|
|
2178
3411
|
}
|
|
2179
3412
|
|
|
@@ -2202,6 +3435,18 @@ window.__ModuleLoader__.load({
|
|
|
2202
3435
|
var dotOn = loggedInSaaS ? (connInfo ? connOnline : serviceRunning) : false;
|
|
2203
3436
|
var dotCls = "dru-dot " + (dotOn ? "dru-dot-on" : "dru-dot-off");
|
|
2204
3437
|
|
|
3438
|
+
// 【0.6.9】「连接偏慢」判定(只用既有字段 + 本地计时,不新增/不假设后端字段):
|
|
3439
|
+
// ① 服务端下发 attempts ≥ 3(已经自动重试多次);② 阶段文案里出现限速/带宽/较慢类提示;
|
|
3440
|
+
// ③ 本地观察到「非 online 阶段持续 ≥ SLOW_CONNECT_HINT_MS」。
|
|
3441
|
+
// 连上(online)或未登录即清零;三者都不成立 → 不显示任何邀请入口(宁可不显示,也不误报)。
|
|
3442
|
+
if (!loggedInSaaS || connPhase === "online") slowConnTrack.since = 0;
|
|
3443
|
+
else if (!slowConnTrack.since) slowConnTrack.since = Date.now();
|
|
3444
|
+
var connSlowText = String(connText || "") + " " + String((connInfo && connInfo.detail) || "");
|
|
3445
|
+
var connSlow = loggedInSaaS && !!connInfo && connPhase !== "online" &&
|
|
3446
|
+
(Number(connInfo.attempts) >= 3 ||
|
|
3447
|
+
/限速|带宽|较慢|拥堵|拥塞/.test(connSlowText) ||
|
|
3448
|
+
(slowConnTrack.since > 0 && Date.now() - slowConnTrack.since >= SLOW_CONNECT_HINT_MS));
|
|
3449
|
+
|
|
2205
3450
|
/**
|
|
2206
3451
|
* 连接阶段区块:把「环境 → bridge 进程 → 中继注册」的自动推进过程如实展示出来,
|
|
2207
3452
|
* 面向非技术用户——非 online 阶段一律说明「正在自动进行,无需操作」;
|
|
@@ -2229,16 +3474,26 @@ window.__ModuleLoader__.load({
|
|
|
2229
3474
|
h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: retryConnect }, busy === "connect-retry" ? "重试中…" : "重试"),
|
|
2230
3475
|
h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: copyDiagnostics }, copiedDiag ? "已复制" : "复制诊断信息")));
|
|
2231
3476
|
rows.push(h("div", { className: "dru-hint", style: { marginTop: 6 } },
|
|
2232
|
-
"
|
|
3477
|
+
"查看日志:后台服务 " + (connInfo.logPath || "") + " ,安装 " + (connInfo.installLogPath || "") + "(把「复制诊断信息」的内容发给客服可加速定位)"));
|
|
2233
3478
|
return h("div", null, rows);
|
|
2234
3479
|
}
|
|
2235
3480
|
// 非错误阶段:只解释「正在自动做什么」,并说明不需要任何操作
|
|
2236
3481
|
if (connInfo.detail) rows.push(h("div", { className: "dru-hint", style: { marginTop: 6 } }, connInfo.detail));
|
|
2237
3482
|
if (connInfo.installing || phase === "no_runtime") {
|
|
2238
3483
|
rows.push(h("div", { className: "dru-hint", style: { marginTop: 4 } },
|
|
2239
|
-
"首次安装会自动下载并配置,期间请不要关闭 DeepSeek
|
|
3484
|
+
"首次安装会自动下载并配置,期间请不要关闭 DeepSeek;装完会自动启动后台服务,无需任何操作。"));
|
|
2240
3485
|
}
|
|
2241
3486
|
if (connInfo.deviceId) rows.push(h("div", { className: "dru-meta" }, "设备 ID:" + connInfo.deviceId));
|
|
3487
|
+
// 【0.6.9】连接明显偏慢时的 🔀 双路块(不打断、不弹窗):把「等」变成一次选择 ——
|
|
3488
|
+
// 升级带宽(转化)or 带新用户换会员时长(拉新,会员同样走这条)。
|
|
3489
|
+
// 判定只用既有字段 —— 服务端重试次数 attempts≥3、或阶段文案里出现限速/带宽/较慢类提示、
|
|
3490
|
+
// 或本地观察「非 online 阶段持续 ≥90s」。判断不了就不显示(宁可不出现,也不误报)。
|
|
3491
|
+
// 放在「立即重试 / 复制诊断信息」之上:先给选择,再给重试(重试仍在,不是死胡同)。
|
|
3492
|
+
// 拉新关闭 + 已是 Pro Max 时 renderDualPath 返回 null → 这块自然不出现(不会留空壳)。
|
|
3493
|
+
if (connSlow) {
|
|
3494
|
+
var dualSlow = renderDualPath("slow");
|
|
3495
|
+
if (dualSlow) rows.push(dualSlow);
|
|
3496
|
+
}
|
|
2242
3497
|
rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
|
|
2243
3498
|
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: retryConnect }, busy === "connect-retry" ? "立即重试中…" : "立即重试"),
|
|
2244
3499
|
h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: copyDiagnostics }, copiedDiag ? "已复制" : "复制诊断信息")));
|
|
@@ -2292,8 +3547,8 @@ window.__ModuleLoader__.load({
|
|
|
2292
3547
|
h("div", { className: "dru-hint", style: { marginTop: 4 } },
|
|
2293
3548
|
!serviceRunning
|
|
2294
3549
|
? (connInfo && connPhase !== "online"
|
|
2295
|
-
? "
|
|
2296
|
-
: "
|
|
3550
|
+
? "本机后台服务正在自动准备中(" + connText + "):连上后这个链接/二维码即可使用,无需其他操作。"
|
|
3551
|
+
: "本机后台服务未运行:先在下方「🖥 后台服务」卡启动。")
|
|
2297
3552
|
: "打开链接/扫码进入即登录态;同设备重复扫码只更新授权,不新增设备。")
|
|
2298
3553
|
)
|
|
2299
3554
|
)
|
|
@@ -2357,7 +3612,7 @@ window.__ModuleLoader__.load({
|
|
|
2357
3612
|
h("button", {
|
|
2358
3613
|
type: "button",
|
|
2359
3614
|
className: "dru-btn dru-btn-ghost",
|
|
2360
|
-
style: { color: "
|
|
3615
|
+
style: { color: "var(--dru-danger)", borderColor: "var(--dru-danger)" },
|
|
2361
3616
|
disabled: devBusy !== "",
|
|
2362
3617
|
onClick: function () { doDeleteDevice(id); }
|
|
2363
3618
|
}, devBusy === "delete:" + id ? "删除中…" : armedDel === id ? "⚠ 再点一次确认删除记录" : "删除记录")
|
|
@@ -2366,7 +3621,7 @@ window.__ModuleLoader__.load({
|
|
|
2366
3621
|
}),
|
|
2367
3622
|
h("div", { className: "dru-hint", style: { marginTop: 4 } },
|
|
2368
3623
|
"取消配对后,对方需重新扫码/登录才能再次远程访问本机。"),
|
|
2369
|
-
h("div", { className: "dru-actions", style: { marginTop: 8, borderTop: "1px dashed
|
|
3624
|
+
h("div", { className: "dru-actions", style: { marginTop: 8, borderTop: "1px dashed var(--dru-border-soft)", paddingTop: 8 } },
|
|
2370
3625
|
h("button", {
|
|
2371
3626
|
type: "button",
|
|
2372
3627
|
className: "dru-btn dru-btn-ghost",
|
|
@@ -2449,7 +3704,7 @@ window.__ModuleLoader__.load({
|
|
|
2449
3704
|
field("手机号", input({ type: "tel", value: rphone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setRphone(e.target.value); } })),
|
|
2450
3705
|
field("密码", input({ type: "password", value: rpass, placeholder: "至少 8 位", autoComplete: "new-password", onChange: function (e) { setRpass(e.target.value); } })),
|
|
2451
3706
|
field("确认密码", input({ type: "password", value: rpass2, placeholder: "再次输入密码", autoComplete: "new-password", onChange: function (e) { setRpass2(e.target.value); } })),
|
|
2452
|
-
field("邀请码(选填)", input({ value: rInvite, placeholder: "
|
|
3707
|
+
field("邀请码(选填)", input({ value: rInvite, placeholder: "邀请你的人的邀请码,如 A8K2M4XQ", autoComplete: "off", onChange: function (e) { setRInvite(e.target.value); } })),
|
|
2453
3708
|
rcap && field("图形验证码", h("div", { className: "dru-captcha" },
|
|
2454
3709
|
input({ value: rcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setRcapTxt(e.target.value); } }),
|
|
2455
3710
|
h("div", { className: "dru-captcha-box", title: "看不清?点击刷新", onClick: function () { loadCaptcha("register"); }, dangerouslySetInnerHTML: rcap.svg && rcap.svg.indexOf("<svg") === 0 ? { __html: rcap.svg } : void 0 },
|
|
@@ -2482,13 +3737,15 @@ window.__ModuleLoader__.load({
|
|
|
2482
3737
|
"服务器地址由你自行部署决定:填写你自建的 dsh-remote 服务地址与访问密钥即可(不是上面的云端地址)。部署方法见 README「自建部署」一节;切回云端随时可登录恢复。")
|
|
2483
3738
|
)
|
|
2484
3739
|
),
|
|
2485
|
-
// Bridge
|
|
2486
|
-
card("🖥
|
|
3740
|
+
// 后台服务(原「Bridge 服务」:面向非技术用户统一叫「后台服务」,卡内配一句人话解释)
|
|
3741
|
+
card("🖥 后台服务",
|
|
2487
3742
|
h("div", { className: "dru-status-line" },
|
|
2488
3743
|
h("span", { className: "dru-dot " + (serviceRunning ? "dru-dot-on" : "dru-dot-off") }),
|
|
2489
3744
|
h("span", null, st ? serviceStateText : "查询中…"),
|
|
2490
3745
|
launchdPid ? h("span", { className: "dru-meta", style: { marginTop: 0 } }, "(pid=" + launchdPid + ")") : null
|
|
2491
3746
|
),
|
|
3747
|
+
h("div", { className: "dru-hint", style: { marginTop: 6 } },
|
|
3748
|
+
"「后台服务」=装在这台电脑上、负责把手机和电脑连起来的小程序(内部名 bridge)。面板会自动安装、自动启动,一般不需要你手动操作;只有状态一直停在「已停止」时才需要点下面的按钮。"),
|
|
2492
3749
|
// 自动连接进度(登录后由面板 2.5s 短轮询自动推进):让用户在 bridge 卡也能一眼看到
|
|
2493
3750
|
// 「到底走到哪一步了」——进程在跑≠能用,注册到中继才算。
|
|
2494
3751
|
connInfo && connPhase && connPhase !== "no_account"
|
|
@@ -2496,21 +3753,21 @@ window.__ModuleLoader__.load({
|
|
|
2496
3753
|
: null,
|
|
2497
3754
|
h("div", { className: "dru-actions", style: { marginTop: 10 } },
|
|
2498
3755
|
!serviceRunning
|
|
2499
|
-
? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: function () { toggleBridge(true); } }, busy === "start" ? "启动中…" : "
|
|
2500
|
-
: h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { toggleBridge(false); } }, busy === "stop" ? "停止中…" : "
|
|
3756
|
+
? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: function () { toggleBridge(true); } }, busy === "start" ? "启动中…" : "启动后台服务")
|
|
3757
|
+
: h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { toggleBridge(false); } }, busy === "stop" ? "停止中…" : "停止后台服务")
|
|
2501
3758
|
),
|
|
2502
3759
|
h("div", { className: "dru-meta" }, st && st.config && st.config.deviceId ? "设备 ID:" + st.config.deviceId : "设备 ID:生成中"),
|
|
2503
3760
|
h("div", { className: "dru-meta" }, st ? (st.service && (st.service.plistExists || st.service.serviceManager === "detached")
|
|
2504
3761
|
? (st.service.serviceManager === "detached"
|
|
2505
3762
|
? (st.service.autostartTask
|
|
2506
3763
|
? "自启动:已注册登录任务(任务计划程序 dsh-remote-bridge)"
|
|
2507
|
-
: "自启动:未注册登录任务(由面板在 dsh web
|
|
3764
|
+
: "自启动:未注册登录任务(由面板在 dsh web 启动时自动拉起后台服务)")
|
|
2508
3765
|
: "自启动服务已安装")
|
|
2509
3766
|
: "自启动服务未安装(启动时自动创建)") : ""),
|
|
2510
3767
|
// 0.6.2:把「运行环境缺失 / launchd 崩溃循环」如实告诉用户,而不是显示「运行中」
|
|
2511
3768
|
st && !serviceRuntimeReady
|
|
2512
3769
|
? h("div", { className: "dru-hint", style: { marginTop: 8 } },
|
|
2513
|
-
"🛠
|
|
3770
|
+
"🛠 在插件市场搜索「dsh-remote-web」装上的只是「面板插件」,这台电脑上的后台服务需要额外补装——已自动在后台安装,装完会自动启动,不用做任何操作。若超过几分钟仍未变成「运行中」,点上方版本卡的「一键更新」手动补全即可。")
|
|
2514
3771
|
: serviceCrashing
|
|
2515
3772
|
? h("div", { className: "dru-hint", style: { marginTop: 8 } },
|
|
2516
3773
|
"⚠️ 上一次启动失败(自启动入口失效),已自动清理失效自启动项并重新补装运行环境。稍候会自动恢复;仍失败请点「一键更新」。")
|
|
@@ -2520,13 +3777,15 @@ window.__ModuleLoader__.load({
|
|
|
2520
3777
|
"⚠️ 设备注册失败:" + (st.service.bindError.message || "未说明原因"),
|
|
2521
3778
|
h("div", { className: "dru-hint", style: { marginTop: 4 } },
|
|
2522
3779
|
st.service.bindError.code === "device_limit_exceeded"
|
|
2523
|
-
? "已达本套餐设备数上限。若是同一台电脑重装,稍等片刻会自动顶替旧设备;仍未恢复请在手机端「设备管理」解绑旧设备(免费用户每月可解绑 3
|
|
3780
|
+
? "已达本套餐设备数上限。若是同一台电脑重装,稍等片刻会自动顶替旧设备;仍未恢复请在手机端「设备管理」解绑旧设备(免费用户每月可解绑 3 次)后,回到这里点「启动后台服务」。"
|
|
2524
3781
|
: "请确认网络与账号状态后重试;仍未解决可点下方「彻底卸载」后重新安装。")
|
|
2525
3782
|
)
|
|
2526
3783
|
: null
|
|
2527
3784
|
),
|
|
2528
3785
|
// 关于 dsh-remote(v0.5+ 远程访问价值说明卡片)
|
|
2529
3786
|
card("📖 关于 dsh-remote", [
|
|
3787
|
+
// 一句话定位(面向非技术用户先讲清楚"这是什么"),下面 4 条为既有技术说明(文案受源码契约锁定)
|
|
3788
|
+
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "📱 一句话:用手机随时接管这台电脑上的 DeepSeek Harness——装上即得专属加密地址,人在外面也能全功能操作(免内网穿透、免公网 IP、全程加密)。"),
|
|
2530
3789
|
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "📱 远程访问:用手机或另一台电脑的浏览器,随时随地使用同一份 dsh web——人在哪都能用(免公网 IP、免内网穿透);官方托管中继,4G/5G 即用,也可自建服务。"),
|
|
2531
3790
|
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。"),
|
|
2532
3791
|
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表。"),
|
|
@@ -2535,7 +3794,7 @@ window.__ModuleLoader__.load({
|
|
|
2535
3794
|
// 只保留结论式的说明句:不再单列「隐私说明: README… · docs/telemetry.md」那一行文档链接
|
|
2536
3795
|
// (面板里堆文档链接既占地方又不像产品文案;完整字段清单在仓库 docs/telemetry.md)。
|
|
2537
3796
|
h("div", { className: "dru-hint", style: { marginTop: 6 } },
|
|
2538
|
-
"📊
|
|
3797
|
+
"📊 匿名装机统计:只上报「装机/连接是否成功」这类事件(安装开始与失败原因、后台服务是否注册成功、是否首次远程打通)——"
|
|
2539
3798
|
+ "不含任何账号、手机号、会话或文件内容、主机名、路径、设备指纹与 IP;标识是本机随机 ID(重装即变)。"
|
|
2540
3799
|
+ "用环境变量 DSH_REMOTE_TELEMETRY=0 可完全关闭(不生成 ID、不发任何请求)。"),
|
|
2541
3800
|
// 加入交流群(后台上传二维码后出现;点击弹出二维码大图便于扫码)
|
|
@@ -2586,7 +3845,7 @@ window.__ModuleLoader__.load({
|
|
|
2586
3845
|
function renderCommunityModal() {
|
|
2587
3846
|
if (!commOpen || !(community && community.qrcode)) return null;
|
|
2588
3847
|
var close = function () { setCommOpen(false); };
|
|
2589
|
-
return h("div", { className: "dru-popup", onMouseDown: function (e) { if (e.target === e.currentTarget) close(); } },
|
|
3848
|
+
return h("div", { className: "dru-popup", "data-dru-theme": theme, onMouseDown: function (e) { if (e.target === e.currentTarget) close(); } },
|
|
2590
3849
|
h("div", { className: "dru-popup-card", role: "dialog", "aria-label": "加入企微交流群" },
|
|
2591
3850
|
h("div", { className: "dru-popup-body" },
|
|
2592
3851
|
h("div", { className: "dru-popup-icon" }, "💬"),
|
|
@@ -2605,12 +3864,13 @@ window.__ModuleLoader__.load({
|
|
|
2605
3864
|
);
|
|
2606
3865
|
}
|
|
2607
3866
|
|
|
2608
|
-
return h("div", { className: "dru-settings-section", role: "region", "aria-label": "远程访问" },
|
|
3867
|
+
return h("div", { className: "dru-settings-section", role: "region", "aria-label": "远程访问", "data-dru-theme": theme },
|
|
2609
3868
|
h("div", { className: "dru-settings-head" },
|
|
2610
3869
|
h("span", { className: "dru-settings-icon" }, "📱"),
|
|
2611
3870
|
h("div", null,
|
|
2612
3871
|
h("h2", { className: "dru-settings-title" }, "远程访问"),
|
|
2613
|
-
h("div", { className: "dru-settings-sub" }, "通过手机或另一台电脑远程使用同一份 dsh web,人在哪都能用(免公网 IP)")
|
|
3872
|
+
h("div", { className: "dru-settings-sub" }, "通过手机或另一台电脑远程使用同一份 dsh web,人在哪都能用(免公网 IP)"),
|
|
3873
|
+
h("div", { className: "dru-settings-sub" }, "装上即得专属加密地址:人在外面也能用手机全功能接管这台电脑上的 DeepSeek Harness(免内网穿透、免公网 IP、全程加密)。")
|
|
2614
3874
|
)
|
|
2615
3875
|
),
|
|
2616
3876
|
h("div", { className: "dru-settings-body" },
|
|
@@ -2667,7 +3927,7 @@ window.__ModuleLoader__.load({
|
|
|
2667
3927
|
),
|
|
2668
3928
|
// 常驻入口最底部:「重启 DeepSeek harness」按钮始终可达(首次安装/更新/排查都用它)
|
|
2669
3929
|
h("div", { className: "dru-restart-foot" },
|
|
2670
|
-
h("div", { className: "dru-hint" }, "重启 dsh web:仅在「刷新页面后仍看不到面板」时才需要。日常安装/更新插件已自动热加载,装完刷新页面即可;重启只影响 dsh web
|
|
3930
|
+
h("div", { className: "dru-hint" }, "重启 dsh web:仅在「刷新页面后仍看不到面板」时才需要。日常安装/更新插件已自动热加载,装完刷新页面即可;重启只影响 dsh web 本身,不影响后台服务与手机端连接。"),
|
|
2671
3931
|
h("button", {
|
|
2672
3932
|
type: "button",
|
|
2673
3933
|
className: "dru-btn " + (restartPending ? "dru-btn-primary" : "dru-btn-ghost"),
|
|
@@ -2701,6 +3961,10 @@ window.__ModuleLoader__.load({
|
|
|
2701
3961
|
dotWatch();
|
|
2702
3962
|
// 侧栏「远程访问」快捷入口(2026-09 恢复注入:与官方「设置」按钮共存不遮挡)
|
|
2703
3963
|
navEnsureStart();
|
|
3964
|
+
// 🎨 主题:**不在这里**建 observer —— 观察器只在真有订阅者时才创建
|
|
3965
|
+
// (面板/弹窗挂载 → themeSubscribe;侧栏入口挂上 → mountNavEntry)。
|
|
3966
|
+
// 这样「没有任何东西需要跟随主题」时不会白留一个 MutationObserver,
|
|
3967
|
+
// 也避免与本插件已有的红点观察器互相干扰。
|
|
2704
3968
|
|
|
2705
3969
|
// 满意度弹窗已停用(不再调度);组件与调度代码保留,便于日后恢复。
|
|
2706
3970
|
var FB_POPUP_RETIRED = true;
|