dsh-remote-web 0.6.9-beta.1 → 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.
Files changed (3) hide show
  1. package/lib/client.js +898 -240
  2. package/lib/index.js +1 -1
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -41,214 +41,370 @@ 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
- ".dru-settings-title{margin:0;font-size:17px;font-weight:600;color:var(--dsw-alias-label-primary,#e6edf3)}",
49
- // 副标题用 #57606a(对 #f6f8fa 卡片底色 6.0:1,达 AA 正文标准);旧的 #8c959f 只有 2.85:1
50
- ".dru-settings-sub{font-size:12.5px;color:var(--dsw-alias-label-tertiary,#57606a);margin-top:3px;line-height:1.6}",
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}",
51
172
  ".dru-settings-body{display:flex;flex-direction:column;gap:14px}",
52
173
  // 首次安装引导小红点(挂在设置页「远程访问」导航栏目右上角)
53
- ".dru-reddot{position:absolute;top:9px;right:12px;width:7px;height:7px;border-radius:50%;background:#e5484d;box-shadow:0 0 0 2px var(--dsw-alias-bg-layer-2,#fff);pointer-events:none;z-index:1}",
54
- ".dru-card{background:#f6f8fa;border:1px solid #eaeef2;border-radius:10px;padding:14px 16px}",
55
- ".dru-card h3{margin:0 0 8px;font-size:13px;font-weight:700;color:#1f2328}",
56
- ".dru-url{background:#ffffff;border:1px solid #d0d7de;border-radius:8px;padding:9px 11px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;word-break:break-all;display:flex;align-items:center;justify-content:space-between;gap:8px;color:#1f2328}",
57
- ".dru-url button{flex:none;min-height:44px;padding:10px 14px;border:1px solid #d0d7de;background:#ffffff;color:#0969da;border-radius:6px;font-size:12.5px;font-weight:600;cursor:pointer;font-family:inherit;transition:background .12s ease,border-color .12s ease}",
58
- ".dru-url button:hover{background:#f6f8fa;border-color:#0969da}",
59
- ".dru-url button:active{background:#eaeef2}",
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)}",
60
181
  ".dru-field{margin-bottom:11px}",
61
- ".dru-field > label{display:block;font-size:12.5px;font-weight:600;color:#1f2328;margin-bottom:5px}",
62
- ".dru-input{width:100%;box-sizing:border-box;min-height:44px;padding:10px 11px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13.5px;font-family:inherit}",
63
- ".dru-input:focus{border-color:#0969da;box-shadow:0 0 0 3px rgba(9,105,218,.28)}",
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)}",
64
185
  ".dru-actions{display:flex;gap:8px;flex-wrap:wrap}",
65
186
  // 触控目标 ≥44px(相邻 8px,见 .dru-actions/.dru-tabs gap):按钮统一 min-height + hover/active 反馈
66
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}",
67
188
  ".dru-btn:disabled{opacity:.55;cursor:default}",
68
- ".dru-btn-primary{background:#0969da;color:#ffffff;border-color:#0969da}",
69
- ".dru-btn-primary:hover:not(:disabled){background:#0860bd}",
70
- ".dru-btn-primary:active:not(:disabled){background:#0757a8;border-color:#0757a8}",
71
- ".dru-btn-ghost{background:#ffffff;color:#1f2328;border-color:#d0d7de}",
72
- ".dru-btn-ghost:hover:not(:disabled){background:#f6f8fa;border-color:#0969da}",
73
- ".dru-btn-ghost:active:not(:disabled){background:#eaeef2}",
74
- ".dru-btn-danger{background:#ffffff;color:#cf222e;border-color:#cf222e}",
75
- ".dru-btn-danger:hover:not(:disabled){background:#fff0f1}",
76
- ".dru-btn-danger:active:not(:disabled){background:#ffdfe0}",
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)}",
77
198
  // 行内文字按钮:视觉高度不变(不破坏排版),用 ::after 把命中区扩到 ≥44px
78
- ".dru-linkbtn{display:inline-block;position:relative;padding:0;border:none;background:none;color:#0969da;font-size:12.5px;line-height:1.7;cursor:pointer;font-family:inherit;text-decoration:none}",
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}",
79
200
  ".dru-linkbtn::after{content:\"\";position:absolute;left:-8px;right:-8px;top:-12px;bottom:-12px}",
80
201
  ".dru-linkbtn:hover{text-decoration:underline}",
81
- ".dru-linkbtn:active{color:#0550ae}",
202
+ ".dru-linkbtn:active{color:var(--dru-accent-fg)}",
82
203
  ".dru-tabs{display:flex;gap:8px;margin-bottom:12px}",
83
- ".dru-tab{flex:1;min-height:44px;display:flex;align-items:center;justify-content:center;padding:8px 0;text-align:center;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;color:#57606a;background:#f6f8fa;border:1px solid #d0d7de;user-select:none;transition:background .12s ease,border-color .12s ease}",
84
- ".dru-tab:hover{background:#ffffff;border-color:#0969da;color:#0969da}",
85
- ".dru-tab.active{color:#0969da;background:#ffffff;border-color:#0969da}",
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)}",
86
207
  ".dru-captcha{display:flex;align-items:stretch;gap:8px}",
87
208
  ".dru-captcha .dru-input{flex:1;min-width:0}",
88
- ".dru-captcha-box{width:118px;height:44px;flex:none;border-radius:8px;border:1px solid #d0d7de;cursor:pointer;background:#f6f8fa;display:flex;align-items:center;justify-content:center;font-size:12px;color:#57606a;overflow:hidden}",
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}",
89
210
  ".dru-captcha-box svg{display:block;width:100%;height:100%}",
90
211
  ".dru-user{display:flex;align-items:center;gap:10px;margin-bottom:12px}",
91
- ".dru-avatar{width:40px;height:40px;border-radius:50%;background:#0969da;color:#ffffff;display:flex;align-items:center;justify-content:center;font-size:17px;font-weight:700;flex-shrink:0}",
92
- ".dru-user .mail{font-size:14px;font-weight:600;color:#1f2328;word-break:break-all}",
93
- ".dru-user .plan{font-size:12px;color:#57606a;margin-top:2px}",
94
- ".dru-status-line{display:flex;align-items:center;gap:8px;font-size:13px;color:#1f2328}",
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)}",
95
216
  ".dru-dot{width:8px;height:8px;border-radius:50%;display:inline-block;flex:none}",
96
- ".dru-dot-on{background:#1a7f37;box-shadow:0 0 6px rgba(26,127,55,.6)}",
97
- ".dru-dot-off{background:#6e7781}",
98
- ".dru-meta{font-size:12px;color:#57606a;margin-top:6px;word-break:break-all}",
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}",
99
220
  ".dru-msg{font-size:12.5px;min-height:18px;margin-top:8px}",
100
- ".dru-msg-ok{color:#1a7f37}",
101
- ".dru-msg-err{color:#cf222e}",
102
- ".dru-msg-warn{color:#9a6700}",
103
- ".dru-hint{font-size:12px;color:#57606a;line-height:1.6}",
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}",
104
225
  // ── 重启 DeepSeek harness(首次安装/更新后置顶提醒 + 底部常驻按钮) ──
105
- ".dru-restart-alert{display:flex;gap:10px;align-items:flex-start;background:#fff8c5;border:1px solid #d4a72c;border-radius:10px;padding:12px 14px;margin-bottom:12px}",
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}",
106
227
  ".dru-restart-alert-icon{font-size:18px;line-height:1.2}",
107
- ".dru-restart-alert-title{font-size:13px;font-weight:700;color:#7d4e00}",
108
- ".dru-restart-alert-sub{font-size:12px;color:#6b5900;margin-top:4px;line-height:1.6}",
109
- ".dru-restart-foot{margin-top:14px;padding-top:12px;border-top:1px solid #eaeef2;display:flex;gap:10px;align-items:center;flex-wrap:wrap}",
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}",
110
231
  ".dru-restart-foot .dru-hint{flex:1;min-width:180px;margin:0}",
111
232
  // ── 用户反馈模块 ──
112
233
  ".dru-fb-tabs{display:flex;gap:8px;margin-bottom:10px}",
113
- ".dru-fb-tab{flex:1;min-height:44px;display:flex;align-items:center;justify-content:center;padding:8px 0;text-align:center;border-radius:8px;cursor:pointer;font-size:12.5px;font-weight:600;color:#57606a;background:#eaeef2;border:1px solid #d0d7de;user-select:none}",
114
- ".dru-fb-tab.active{color:#0969da;background:#ffffff;border-color:#0969da}",
115
- ".dru-fb-select{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13.5px;font-family:inherit}",
116
- ".dru-fb-textarea{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13.5px;font-family:inherit;resize:vertical;min-height:64px}",
117
- ".dru-fb-textarea:focus{border-color:#0969da;box-shadow:0 0 0 3px rgba(9,105,218,.28)}",
118
- ".dru-fb-select:focus{border-color:#0969da;box-shadow:0 0 0 3px rgba(9,105,218,.28)}",
119
- ".dru-fb-item{border:1px solid #eaeef2;border-radius:8px;background:#ffffff;padding:10px 12px;margin-bottom:8px}",
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)}",
120
241
  ".dru-fb-item-head{display:flex;align-items:center;gap:8px;margin-bottom:4px;flex-wrap:wrap}",
121
242
  ".dru-fb-badge{font-size:11px;border-radius:999px;padding:1px 8px;flex:none}",
122
- ".dru-fb-badge-open{color:#9a6700;background:#fff8c5;border:1px solid #eed888}",
123
- ".dru-fb-badge-processing{color:#0969da;background:#ddf4ff;border:1px solid #b6e3ff}",
124
- ".dru-fb-badge-done{color:#1a7f37;background:#dafbe1;border:1px solid #aceebb}",
125
- ".dru-fb-cat{font-size:11px;border-radius:999px;padding:1px 8px;flex:none;color:#57606a;background:#f6f8fa;border:1px solid #d0d7de}",
126
- ".dru-fb-item-title{font-size:13px;font-weight:600;color:#1f2328;flex:1;min-width:120px}",
127
- ".dru-fb-item-content{font-size:12.5px;color:#57606a;white-space:pre-wrap;word-break:break-word;margin:4px 0}",
128
- ".dru-fb-item-time{font-size:11.5px;color:#57606a}",
129
- ".dru-fb-reply{border-top:1px dashed #eaeef2;margin-top:8px;padding-top:8px}",
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}",
130
251
  ".dru-fb-reply-row{display:flex;gap:6px;align-items:flex-start;margin-bottom:6px}",
131
- ".dru-fb-reply-who{font-size:12px;font-weight:600;color:#0969da;flex:none;width:76px}",
132
- ".dru-fb-reply-who.user{color:#57606a}",
133
- ".dru-fb-reply-text{font-size:12.5px;color:#1f2328;white-space:pre-wrap;word-break:break-word;flex:1}",
134
- ".dru-fb-reply-input{width:100%;box-sizing:border-box;padding:7px 10px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;font-size:12.5px;font-family:inherit;resize:vertical;min-height:44px}",
135
- ".dru-fb-empty{font-size:12.5px;color:#57606a;text-align:center;padding:14px 0}",
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}",
136
257
  // ── 满意度弹窗(1 小时体验后,只弹一次) ──
137
- ".dru-popup{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:2147482000;display:flex;align-items:center;justify-content:center;padding:24px;box-sizing:border-box}",
138
- ".dru-popup-card{width:min(400px,calc(100vw - 48px));background:#ffffff;color:#1f2328;border:1px solid #d0d7de;border-radius:12px;box-shadow:0 24px 64px rgba(0,0,0,.45);font-size:14px;line-height:1.5;font-family:var(--dsw-font-family,-apple-system,'PingFang SC','Microsoft YaHei',sans-serif);overflow:hidden}",
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}",
139
260
  ".dru-popup-body{padding:22px 22px 16px;text-align:center}",
140
261
  ".dru-popup-icon{font-size:34px;margin-bottom:8px}",
141
- ".dru-popup-title{font-size:16px;font-weight:700;color:#1f2328;margin-bottom:6px}",
142
- ".dru-popup-sub{font-size:12.5px;color:#57606a;margin-bottom:16px}",
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}",
143
264
  ".dru-popup-rate{display:flex;gap:10px;justify-content:center;margin-bottom:14px}",
144
- ".dru-popup-rate button{flex:1;max-width:96px;border:1px solid #d0d7de;background:#f6f8fa;border-radius:10px;padding:12px 6px;font-size:20px;cursor:pointer;font-family:inherit}",
145
- ".dru-popup-rate button:hover{border-color:#0969da;background:#ddf4ff}",
146
- ".dru-popup-rate button.sel{border-color:#0969da;background:#ddf4ff;box-shadow:0 0 0 3px rgba(9,105,218,.15)}",
147
- ".dru-popup-rate button .lbl{display:block;font-size:11px;color:#57606a;margin-top:4px;font-weight:600}",
148
- ".dru-popup-textarea{width:100%;box-sizing:border-box;padding:9px 11px;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;color:#1f2328;font-size:13px;font-family:inherit;resize:vertical;min-height:56px;text-align:left}",
149
- ".dru-popup-invite{background:#f6f8fa;border:1px dashed #d0d7de;border-radius:8px;padding:12px;font-size:13px;color:#57606a;text-align:center;margin-bottom:12px}",
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}",
150
271
  ".dru-popup-actions{display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-top:14px}",
151
- ".dru-popup-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 16px;border-top:1px solid #eaeef2;font-size:12px;color:#57606a}",
152
- ".dru-popup-foot button{min-height:44px;border:none;background:none;color:#57606a;cursor:pointer;font-size:12.5px;font-family:inherit;padding:10px 8px;border-radius:6px}",
153
- ".dru-popup-foot button:hover{color:#0969da;background:#f6f8fa}",
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)}",
154
275
  ".dru-popup .dru-msg{text-align:left}",
155
- ".dru-community-qr{display:block;width:220px;max-width:62vw;margin:0 auto;background:#ffffff;padding:10px;border-radius:10px;border:1px solid #d0d7de;box-sizing:content-box}",
156
- ".dru-restart-auto{display:flex;align-items:center;gap:8px;margin-top:10px;font-size:12.5px;color:#1a7f37;background:#dafbe1;border-radius:8px;padding:8px 10px}",
157
- ".dru-restart-auto.muted{color:#57606a;background:#f6f8fa}",
158
- ".dru-fb-community{margin-top:16px;padding-top:14px;border-top:1px dashed #d0d7de;text-align:center}",
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}",
159
280
  ".dru-fb-community .dru-community-qr{width:180px;max-width:56vw}",
160
281
  // ── 自管理:版本与更新(插件面板内提供在线更新/彻底卸载,市场无更新按钮) ──
161
282
  ".dru-ver-badge{display:inline-block;font-size:11px;border-radius:999px;padding:1px 8px;margin-left:6px;vertical-align:1px}",
162
- ".dru-ver-badge-new{color:#9a6700;background:#fff8c5;border:1px solid #eed888}",
163
- ".dru-ver-badge-ok{color:#1a7f37;background:#dafbe1;border:1px solid #aceebb}",
164
- ".dru-up-log{margin-top:8px;background:#0d1117;color:#e6edf3;border-radius:8px;padding:8px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:150px;overflow:auto}",
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}",
165
286
  // 版本卡信息层级(0.6.9):版本号大字 + 通道 chip + 指标行(已用时/无输出)+ 卡住告警
166
287
  ".dru-ver-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
167
- ".dru-ver-num{font-size:15px;font-weight:700;color:#1f2328}",
168
- ".dru-ver-chip{font-size:11.5px;border-radius:999px;padding:2px 9px;color:#0a3069;background:#ddf4ff;border:1px solid #b6e3ff;white-space:nowrap}",
169
- ".dru-ver-metrics{margin-top:9px;background:#ffffff;border:1px solid #d0d7de;border-radius:8px;padding:9px 11px}",
170
- ".dru-ver-metric{display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12.5px;line-height:1.9;color:#1f2328}",
171
- ".dru-ver-metric > span:first-child{color:#57606a}",
172
- ".dru-stall{margin-top:9px;background:#fff8c5;border:1px solid #d4a72c;border-radius:8px;padding:10px 12px;font-size:12.5px;line-height:1.65;color:#6b5900}",
173
- ".dru-stall b{color:#7d4e00}",
174
- ".dru-spin{width:13px;height:13px;border-radius:50%;border:2px solid #d0d7de;border-top-color:#0969da;display:inline-block;vertical-align:-2px;animation:dru-spin .9s linear infinite}",
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}",
175
296
  "@keyframes dru-spin{to{transform:rotate(360deg)}}",
176
- // ── 🎯 邀请好友(0.6.9):奖励前置(大字)→ 进度 → 邀请码/链接(一键复制)→ 折叠规则 → 记录 ──
177
- ".dru-invite-hero{font-size:20px;line-height:1.4;font-weight:700;color:#1f2328;margin:2px 0 8px}",
178
- ".dru-invite-hero em{font-style:normal;color:#0550ae}",
179
- ".dru-invite-sub{font-size:13px;line-height:1.65;color:#57606a}",
180
- ".dru-invite-prog{margin-top:12px;background:#ffffff;border:1px solid #d0d7de;border-radius:8px;padding:10px 12px}",
181
- ".dru-invite-prog-top{display:flex;align-items:baseline;justify-content:space-between;gap:8px;flex-wrap:wrap;font-size:13px;font-weight:600;color:#1f2328}",
182
- ".dru-invite-prog-top .n{font-size:15px;font-weight:700;color:#0550ae}",
183
- ".dru-invite-bar{height:8px;border-radius:999px;background:#eaeef2;overflow:hidden;margin:9px 0 7px}",
184
- ".dru-invite-bar-fill{height:100%;border-radius:999px;background:#0969da;transition:width .3s ease}",
185
- ".dru-invite-next{font-size:12.5px;line-height:1.65;color:#57606a}",
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)}",
186
308
  ".dru-copy-row{margin-top:12px}",
187
309
  ".dru-copy-row:first-child{margin-top:4px}",
188
- ".dru-copy-label{font-size:12.5px;font-weight:600;color:#1f2328;margin-bottom:6px}",
189
- ".dru-copy-box{display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:#ffffff;border:1px solid #d0d7de;border-radius:8px;padding:9px 11px}",
190
- ".dru-code-val{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:19px;font-weight:700;letter-spacing:2.5px;color:#1f2328;flex:1;min-width:140px;word-break:break-all}",
191
- ".dru-link-val{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;line-height:1.55;color:#1f2328;flex:1;min-width:180px;word-break:break-all;overflow-wrap:anywhere}",
192
- ".dru-copy-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;flex:none;min-height:44px;padding:10px 16px;border-radius:8px;border:1px solid #0969da;background:#0969da;color:#ffffff;font:600 13px/1.2 inherit;cursor:pointer;transition:background .12s ease,border-color .12s ease}",
193
- ".dru-copy-btn:hover:not(:disabled){background:#0860bd;border-color:#0860bd}",
194
- ".dru-copy-btn:active:not(:disabled){background:#0757a8;border-color:#0757a8}",
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)}",
195
317
  ".dru-copy-btn:disabled{opacity:.55;cursor:default}",
196
- ".dru-copy-btn.copied{background:#1a7f37;border-color:#1a7f37}",
197
- ".dru-empty{text-align:center;padding:20px 14px;background:#ffffff;border:1px dashed #d0d7de;border-radius:10px}",
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}",
198
320
  ".dru-empty-icon{font-size:26px;line-height:1;margin-bottom:8px}",
199
- ".dru-empty-title{font-size:13.5px;font-weight:600;color:#1f2328;margin-bottom:5px}",
200
- ".dru-empty-sub{font-size:12.5px;line-height:1.7;color:#57606a;max-width:380px;margin:0 auto}",
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}",
201
323
  ".dru-empty-actions{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:14px}",
202
- ".dru-disclose{width:100%;box-sizing:border-box;min-height:44px;display:flex;align-items:center;justify-content:space-between;gap:10px;background:#ffffff;border:1px solid #d0d7de;border-radius:8px;padding:10px 12px;font:600 13px/1.3 inherit;color:#1f2328;cursor:pointer;text-align:left}",
203
- ".dru-disclose:hover{background:#f6f8fa;border-color:#0969da}",
204
- ".dru-disclose-caret{font-size:12px;font-weight:600;color:#57606a;flex:none}",
205
- ".dru-rules{margin:10px 0 0;padding-left:20px;font-size:12.5px;line-height:1.8;color:#57606a}",
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)}",
206
328
  ".dru-rules li{margin-bottom:6px}",
207
- ".dru-rules b{color:#1f2328}",
208
- ".dru-rec{background:#ffffff;border:1px solid #eaeef2;border-radius:8px;padding:10px 12px;margin-bottom:8px}",
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}",
209
331
  ".dru-rec-top{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
210
- ".dru-rec-who{font-size:13px;font-weight:600;color:#1f2328;flex:1;min-width:120px;word-break:break-all}",
211
- ".dru-rec-when{font-size:12px;color:#57606a}",
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)}",
212
334
  ".dru-rec-tag{font-size:11.5px;border-radius:999px;padding:1px 9px;flex:none;white-space:nowrap}",
213
- ".dru-rec-tag-ok{color:#116329;background:#dafbe1;border:1px solid #aceebb}",
214
- ".dru-rec-tag-wait{color:#9a6700;background:#fff8c5;border:1px solid #eed888}",
215
- ".dru-rec-sub{font-size:12px;color:#57606a;margin-top:5px;line-height:1.6}",
216
- ".dru-rec-head{display:flex;gap:8px;font-size:11.5px;font-weight:600;color:#57606a;padding:0 4px 7px;border-bottom:1px solid #eaeef2;margin-bottom:9px}",
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)}}",
217
368
  // ── 📱 远程访问(一次性访问密钥 + 已授权设备管理) ──
218
369
  ".dru-access-flex{display:flex;gap:14px;flex-wrap:wrap;align-items:flex-start;margin-top:10px}",
219
370
  ".dru-access-col{flex:1;min-width:230px;display:flex;flex-direction:column;gap:8px}",
220
- ".dru-qr-img{width:180px;height:180px;flex:none;border-radius:8px;border:1px solid #d0d7de;background:#ffffff;object-fit:contain}",
221
- ".dru-qr-ph{width:180px;height:180px;flex:none;border-radius:8px;border:1px dashed #d0d7de;background:#f6f8fa;color:#57606a;font-size:12px;display:flex;align-items:center;justify-content:center;text-align:center;padding:10px;box-sizing:border-box}",
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}",
222
373
  ".dru-url.big{font-size:13.5px;font-weight:600}",
223
- ".dru-cd{font-size:12px;color:#9a6700;margin-top:2px}",
224
- ".dru-cd-ok{color:#1a7f37}",
225
- ".dru-key-note{font-size:12px;color:#57606a;line-height:1.6}",
226
- ".dru-dev{border:1px solid #eaeef2;border-radius:8px;background:#ffffff;padding:9px 11px;margin-bottom:8px}",
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}",
227
378
  ".dru-dev-top{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
228
- ".dru-dev-name{font-size:13px;font-weight:600;color:#1f2328;flex:1;min-width:130px}",
229
- ".dru-dev-meta{font-size:11.5px;color:#57606a}",
230
- ".dru-dev-sub{font-size:11.5px;color:#57606a;margin-top:4px}",
231
- ".dru-dev-tag{font-size:11.5px;color:#57606a;border:1px solid #d0d7de;border-radius:999px;padding:0 7px;flex:none;white-space:nowrap}",
232
- ".dru-dev-tag-off{color:#cf222e;border-color:#ffb3b6;background:#fff0f1}",
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)}",
233
384
  // Phase-5:端到端加密(E2EE)状态行 —— 启用=绿字绿点,未启用=灰字(纯文字状态行,不打扰)
234
- ".dru-e2ee-line{display:flex;align-items:center;gap:7px;margin-top:6px;font-size:12px;line-height:1.5;color:#57606a}",
235
- ".dru-e2ee-line.ok{color:#1a7f37}",
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)}",
236
387
  // 侧栏「远程访问」快捷按钮(2026-09 恢复注入,与官方「设置」按钮共存不遮挡):
237
388
  // 独立 button(非 clone),插在官方「设置」按钮之前,同源同布局不覆盖官方热区。
238
- ".dru-nav-remote{display:inline-flex;align-items:center;gap:6px;height:30px;margin:0 0 2px;padding:0 10px;border:1px solid #d0d7de;border-radius:8px;background:#ffffff;color:#0969da;font:500 12.5px/1 inherit;cursor:pointer;white-space:nowrap;user-select:none}",
239
- ".dru-nav-remote:hover:not(:disabled){background:#f0f6ff;border-color:#0969da}",
240
- ".dru-nav-remote-dot{position:absolute;top:-2px;right:-2px;width:8px;height:8px;border-radius:50%;background:#e5484d;box-shadow:0 0 0 2px #fff;pointer-events:none}",
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}",
241
392
  // 侧栏入口与官方按钮同高(30px);用 ::after 把命中区补到 ≥44px,不破坏与原按钮的对齐
242
393
  ".dru-nav-remote::after{content:\"\";position:absolute;left:0;right:0;top:-7px;bottom:-7px}",
243
394
  // ── 无障碍基线(0.6.9)────────────────────────────────────────────────
244
395
  // 键盘焦点必须可见:放在样式表末尾,覆盖上面任何 outline:none(同特异性后者胜)
245
- ".dru-settings-section :focus-visible,.dru-popup :focus-visible{outline:2px solid #0969da;outline-offset:2px}",
396
+ ".dru-settings-section :focus-visible,.dru-popup :focus-visible{outline:2px solid var(--dru-focus);outline-offset:2px}",
246
397
  ".dru-settings-section [role=\"region\"]:focus-visible{outline-offset:-2px}",
247
- ".dru-btn:focus-visible,.dru-copy-btn:focus-visible,.dru-tab:focus-visible,.dru-disclose:focus-visible{outline:2px solid #0969da;outline-offset:2px;box-shadow:0 0 0 4px rgba(9,105,218,.18)}",
248
- // 尊重系统「减少动态效果」:关掉过渡与旋转动画(状态文字照常更新)
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 时没有动效」的要求。
249
403
  "@media (prefers-reduced-motion: reduce){",
250
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}",
251
- " .dru-spin{animation:none !important;border-top-color:#0969da}",
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}",
252
408
  "}",
253
409
  // 窄屏(手机竖屏打开本机面板):卡片内边距收紧、代码/链接不溢出
254
410
  "@media (max-width:520px){",
@@ -256,10 +412,137 @@ window.__ModuleLoader__.load({
256
412
  " .dru-invite-hero{font-size:18px}",
257
413
  " .dru-copy-box{flex-direction:column;align-items:stretch}",
258
414
  " .dru-copy-btn{width:100%}",
415
+ // 气泡独占一行,绝不把按钮挤到屏幕外(320px 下也不横向滚动)
416
+ " .dru-tip{flex:1 1 100%;padding:9px 40px 9px 10px;font-size:12px}",
259
417
  "}",
260
418
  ].join("\n");
261
419
  document.head.appendChild(styleEl);
262
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
+
263
546
  // ── 首次安装引导小红点(设置页「远程访问」栏目,localStorage 控制) ────
264
547
  // 无 dsh-remote-seen-dot key 视为首次:在设置页导航栏目右上角显示 CSS 圆点;
265
548
  // 点击栏目/红点后写入 key(之后不再显示),重启 DSH Web 不复发。
@@ -281,6 +564,23 @@ window.__ModuleLoader__.load({
281
564
  }
282
565
  window.__dshRemoteDotWatch = false;
283
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
+ }
284
584
  /** 给单个设置页导航栏目按钮挂红点(幂等)。 */
285
585
  function dotInject(cell) {
286
586
  try {
@@ -390,6 +690,12 @@ window.__ModuleLoader__.load({
390
690
  btn.id = NAV_ENTRY_ID;
391
691
  btn.className = "dru-nav-remote";
392
692
  btn.setAttribute("data-dru-remote", "1");
693
+ // 🎨 侧栏入口在面板之外,拿不到面板根上的令牌 → 自己也挂一份主题属性
694
+ btn.setAttribute("data-dru-theme", themeGet());
695
+ themeState.navEl = btn;
696
+ // 入口常驻,所以它自己就是主题观察器的一个订阅者:
697
+ // 用户切深色时(哪怕面板没开着)这里也要立刻跟上。
698
+ themeStartWatch();
393
699
  btn.title = "打开「远程访问」设置(快捷入口)";
394
700
  btn.style.position = "relative";
395
701
  btn.textContent = "📱 远程访问";
@@ -563,7 +869,7 @@ window.__ModuleLoader__.load({
563
869
  if (m < 60) return m + " 分 " + (s % 60 < 10 ? "0" : "") + (s % 60) + " 秒";
564
870
  return Math.floor(m / 60) + " 小时 " + (m % 60 < 10 ? "0" : "") + (m % 60) + " 分";
565
871
  }
566
- /** 手机号掩码(邀请记录里的好友手机号:面板只展示掩码,完整号码留在服务端)。 */
872
+ /** 手机号掩码(邀请记录里的新用户手机号:面板只展示掩码,完整号码留在服务端)。 */
567
873
  function maskPhoneLike(v) {
568
874
  var s = String(v == null ? "" : v).trim();
569
875
  if (!s) return "";
@@ -592,12 +898,72 @@ window.__ModuleLoader__.load({
592
898
  var connFailRef = { v: 0 };
593
899
  var CONN_FAIL_VISIBLE = 3; // ≈ 2.5s × 3:既要够快让用户看见,又要避免一次抖动就报红
594
900
  /**
595
- * 「连接偏慢」的本地观察起点(0 = 不慢)——用于在用户干等时给一个轻量邀请入口。
901
+ * 「连接偏慢」的本地观察起点(0 = 不慢)——用于在用户干等时给出「转化 or 拉新」双路块。
596
902
  * 只用既有 /dsh-remote/bridge-status 下发字段 + 本地计时,**不新增也不假设后端字段**;
597
903
  * 观察不到就永远不显示(宁可不出现,也不误报)。
598
904
  */
599
905
  var slowConnTrack = { since: 0 };
600
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
+ }
601
967
  /** 连接阶段轮询节奏:未连通时 2.5s(自动推进),已连接后退避到 15s;页面隐藏时完全停下。 */
602
968
  var CONN_POLL_FAST_MS = 2500;
603
969
  var CONN_POLL_SLOW_MS = 15000;
@@ -1003,6 +1369,9 @@ window.__ModuleLoader__.load({
1003
1369
  }
1004
1370
 
1005
1371
  function FeedbackPopup() {
1372
+ // 🎨 主题:弹窗是 shell.overlay 下的独立根(不在 .dru-settings-section 里),
1373
+ // 同样要拿一份 data-dru-theme 才有深色令牌;放在 useState 之前不扰动 hook 序号。
1374
+ var theme = useSyncExternalStore(themeSubscribe, themeGet);
1006
1375
  var open = usePopupOpen();
1007
1376
  var stepArr = useState("rate"); var step = stepArr[0]; var setStep = stepArr[1];
1008
1377
  var ratingArr = useState(null); var rating = ratingArr[0]; var setRating = ratingArr[1];
@@ -1023,7 +1392,19 @@ window.__ModuleLoader__.load({
1023
1392
  if (!acct || !acct.invite_code) { setPopupInvite({ code: "" }); return; }
1024
1393
  api("/dsh-remote/remote-url").then(function (u) {
1025
1394
  var base = (u && u.remoteUrl) || "https://n.risegao.cn:13443/app/";
1026
- setPopupInvite({ code: acct.invite_code, link: base.replace(/\/+$/, "") + "/?invite=" + encodeURIComponent(acct.invite_code) });
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
+ });
1027
1408
  }).catch(function () { setPopupInvite({ code: acct.invite_code }); });
1028
1409
  }).catch(function () { setPopupInvite({ code: "" }); });
1029
1410
  }
@@ -1035,6 +1416,13 @@ window.__ModuleLoader__.load({
1035
1416
  api("/dsh-remote/feedback-config").then(function (b) {
1036
1417
  setFbAuth(b.auth === "account" ? "account" : "anonymous");
1037
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 () {});
1038
1426
  // eslint-disable-next-line react-hooks/exhaustive-deps
1039
1427
  }, [open]);
1040
1428
 
@@ -1065,7 +1453,7 @@ window.__ModuleLoader__.load({
1065
1453
  .finally(function () { setBusy(""); });
1066
1454
  };
1067
1455
 
1068
- 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(); } },
1069
1457
  h("div", { className: "dru-popup-card", role: "dialog", "aria-label": "用户反馈" },
1070
1458
  h("div", { className: "dru-popup-body" },
1071
1459
  step === "rate"
@@ -1092,11 +1480,22 @@ window.__ModuleLoader__.load({
1092
1480
  : h("div", null,
1093
1481
  h("div", { className: "dru-popup-icon" }, "🤝"),
1094
1482
  h("div", { className: "dru-popup-title" }, "愿意推荐给朋友吗?"),
1095
- h("div", { className: "dru-popup-sub" }, "邀请好友一起使用,双方都能获得更好的体验"),
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 并上线后自动到账(奖励只发给邀请人)。"),
1096
1493
  recommend === true
1097
1494
  ? h("div", { className: "dru-popup-invite" },
1098
1495
  h("div", null, "🎉 感谢推荐!"),
1099
- popupInvite && popupInvite.code && popupInvite.link
1496
+ popupInvite && popupInvite.inviteOff
1497
+ ? h("div", { style: { marginTop: 6, fontSize: 12 } }, "当前未开放带新用户换会员时长的活动,暂时不需要邀请链接。")
1498
+ : popupInvite && popupInvite.code && popupInvite.link
1100
1499
  ? h("div", null,
1101
1500
  h("div", { className: "dru-url", style: { marginTop: 10, textAlign: "left" } },
1102
1501
  h("span", null, popupInvite.link),
@@ -1107,19 +1506,21 @@ window.__ModuleLoader__.load({
1107
1506
  });
1108
1507
  } }, inviteCopied ? "已复制" : "复制邀请链接")
1109
1508
  ),
1110
- h("div", { style: { marginTop: 8, fontSize: 12 } }, "把链接发给好友,注册时自动带上你的邀请码。")
1509
+ h("div", { style: { marginTop: 8, fontSize: 12 } }, "把链接发给一位还没注册过的新用户,对方注册时会自动带上你的邀请码。")
1111
1510
  )
1112
1511
  : h("div", { style: { marginTop: 6, fontSize: 12 } },
1113
1512
  popupInvite && popupInvite.code
1114
- ? "邀请码 " + popupInvite.code + " 已生成:在设置面板 →「🎯 邀请好友,一起用远程访问」里复制邀请链接。"
1115
- : "登录手机号账号后,在设置面板 →「🎯 邀请好友,一起用远程访问」里获取专属邀请链接。")
1513
+ ? "邀请码 " + popupInvite.code + " 已生成:在设置面板 →「🎁 带新用户,换会员时长」里复制邀请链接。"
1514
+ : "登录手机号账号后,在设置面板 →「🎁 带新用户,换会员时长」里获取专属邀请链接。")
1116
1515
  )
1117
1516
  : h("div", null,
1118
1517
  h("div", { className: "dru-popup-actions" },
1119
1518
  h("button", { type: "button", className: "dru-btn dru-btn-primary", onClick: function () { setRecommend(true); loadPopupInvite(); } }, "愿意推荐"),
1120
1519
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: function () { setRecommend(false); } }, "暂时不了")
1121
1520
  ),
1122
- h("div", { className: "dru-hint", style: { marginTop: 10 } }, "推荐成功可获得专属邀请链接")
1521
+ popupInvite && popupInvite.inviteOff
1522
+ ? null
1523
+ : h("div", { className: "dru-hint", style: { marginTop: 10 } }, "推荐成功可获得你的专属邀请链接(带新用户换会员时长)")
1123
1524
  ),
1124
1525
  message && h("div", { className: "dru-msg dru-msg-" + message.kind }, message.text),
1125
1526
  recommend !== null && h("div", { className: "dru-popup-actions" },
@@ -1509,6 +1910,10 @@ window.__ModuleLoader__.load({
1509
1910
  // 匿名装机统计的「面板打开」只上报一次/每页(node 半还会每进程去重)。
1510
1911
  var panelOpenedSent = false;
1511
1912
  function RemoteControlSection(props) {
1913
+ // 🎨 主题(宿主深色/亮色):订阅外部存储 → 宿主切换时本组件自动重渲染,
1914
+ // 根节点上的 data-dru-theme 随之更新,整套 --dru-* 令牌实时切换。
1915
+ // 放在全部 useState 之前:不改变既有 hook 序号(useSyncExternalStore 与 useState 各自计数)。
1916
+ var theme = useSyncExternalStore(themeSubscribe, themeGet);
1512
1917
 
1513
1918
  var statusArr = useState(null); var st = statusArr[0]; var setSt = statusArr[1];
1514
1919
  var modeArr = useState("saas"); var mode = modeArr[0]; var setMode = modeArr[1]; // saas | local
@@ -1581,12 +1986,15 @@ window.__ModuleLoader__.load({
1581
1986
  var connArr = useState(null); var conn = connArr[0]; var setConn = connArr[1];
1582
1987
  var copiedDiagArr = useState(false); var copiedDiag = copiedDiagArr[0]; var setCopiedDiag = copiedDiagArr[1];
1583
1988
 
1584
- // ── 🎯 邀请视图(0.6.9 重排)状态:放在全部既有字段之后,保持既有 hook 序号不变 ──
1989
+ // ── 🎁 邀请视图(带新用户换会员)状态:放在全部既有字段之后,保持既有 hook 序号不变 ──
1585
1990
  var invLoadArr = useState(false); var inviteLoading = invLoadArr[0]; var setInviteLoading = invLoadArr[1]; // 记录/规则读取中
1586
1991
  var invErrArr = useState(null); var inviteErr = invErrArr[0]; var setInviteErr = invErrArr[1]; // 记录读取失败原因(≠空记录)
1587
1992
  var invRulesArr = useState(false); var inviteRulesOpen = invRulesArr[0]; var setInviteRulesOpen = invRulesArr[1]; // 活动规则默认折叠
1588
1993
  var invCopyArr = useState(""); var inviteCopied = invCopyArr[0]; var setInviteCopied = invCopyArr[1]; // "" | "code" | "link"(复制成功反馈)
1589
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];
1590
1998
 
1591
1999
  var refresh = useCallback(function () {
1592
2000
  setBusy("status");
@@ -2361,7 +2769,7 @@ window.__ModuleLoader__.load({
2361
2769
  /**
2362
2770
  * 拉取邀请视图数据:公共配置(邀请规则/试用天数)+ 我的邀请记录。
2363
2771
  * 【0.6.9 修正】记录拉取失败**不再静默当成"没有记录"**:否则用户会以为
2364
- * "好友明明装好了却没记上"。失败时记下原因,由列表位置给出「重试」。
2772
+ * "新用户明明装好了却没记上"。失败时记下原因,由列表位置给出「重试」。
2365
2773
  */
2366
2774
  var loadInvite = function () {
2367
2775
  setInviteLoading(true);
@@ -2390,18 +2798,208 @@ window.__ModuleLoader__.load({
2390
2798
  var input = function (attrs) { return h("input", Object.assign({ className: "dru-input", type: "text" }, attrs)); };
2391
2799
  var card = function (title, children) { return h("div", { className: "dru-card" }, title ? h("h3", null, title) : null, children); };
2392
2800
  /**
2393
- * 轻量邀请入口(0.6.9):额度将尽 / 连接偏慢时用的**文字链**层级入口——
2394
- * 与主 CTA(升级 PRO / 重试)并列但不抢焦点,不弹窗、不打断。
2801
+ * 邀请奖励口径:优先 public-config 下发的 invite_rule,取不到才用与线上一致的兜底常量。
2802
+ * off=true(n / days 被显式配成 0)表示拉新已关闭 → 所有拉新入口/卡片整体隐藏。
2395
2803
  */
2396
- function renderInviteLink(label, title) {
2397
- return h("button", {
2398
- type: "button",
2399
- className: "dru-linkbtn",
2400
- style: { margin: "0 4px" },
2401
- title: title || label,
2402
- disabled: busy !== "",
2403
- onClick: function () { setView("invite"); loadInvite(); }
2404
- }, label);
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
+ );
2405
3003
  }
2406
3004
 
2407
3005
  // ---------- 我的信息(个人中心) ----------
@@ -2434,17 +3032,21 @@ window.__ModuleLoader__.load({
2434
3032
  else if (source === "trial") planText = "试用 PRO 会员 · 到期 " + fmtDate(a.trial_expires_at);
2435
3033
  else if (endsAt) planText = plan === "pro_max" ? "Pro Max 会员 · 到期 " + fmtDate(endsAt) : "PRO 会员 · 到期 " + fmtDate(endsAt);
2436
3034
  else planText = plan === "pro_max" ? "Pro Max 长期会员" : "PRO 长期会员";
2437
- // 【0.6.9】额度将尽时的轻量邀请入口:只依据服务端已下发的额度字段判断
3035
+ // 【0.6.9】「额度将尽 / 已被限速」判定:只依据服务端已下发的额度字段
2438
3036
  // · limit_enabled 且 percent ≥ 80(接近用尽)
2439
3037
  // · limit_enabled 但没下发 max_mbps(文案已显示「已限速」=正在被限速)
2440
3038
  // 判断不了(quota 缺失、不限量、会员)→ 不显示,绝不误报。
3039
+ // 注意:会员(PRO / Pro Max)的额度不吃这条免费档判断,所以「升级」永远不会弹给付费用户,
3040
+ // 但连接偏慢那条(renderConnectBlock)对会员同样成立 —— 会员也会遇到慢,那时只剩拉新路。
2441
3041
  var quotaPctNum = Number(quotaPct);
2442
3042
  var quotaTight = !!(quota && quota.limit_enabled && !isMember &&
2443
3043
  ((isFinite(quotaPctNum) && quotaPctNum >= 80) || !quota.max_mbps));
2444
- // 邀请规则(公共配置下发;缺省时用与邀请页一致的兜底值,不编造)
2445
- var invRule = (pub && pub.invite_rule) || {};
2446
- var invRuleN = Number(invRule.n) > 0 ? Number(invRule.n) : 3;
2447
- var invRuleDays = Number(invRule.days) > 0 ? Number(invRule.days) : 15;
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;
2448
3050
  return h("div", null,
2449
3051
  h("div", { className: "dru-user" },
2450
3052
  h("div", { className: "dru-avatar" }, (st.config.phone || "D").charAt(0).toUpperCase()),
@@ -2461,21 +3063,27 @@ window.__ModuleLoader__.load({
2461
3063
  h("div", { className: "dru-actions", style: { marginTop: 10 } },
2462
3064
  h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { display: "inline-flex", alignItems: "center" }, disabled: busy !== "", title: "升级/续费(带登录态打开)", onClick: openUpgradeAuth },
2463
3065
  busy === "upgrade" ? "生成链接中…" : (!isMember ? "🚀 升级 PRO" : source === "trial" ? "🚀 转正式 PRO" : "🔄 续费会员")),
2464
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("invite"); loadInvite(); } }, "🎯 邀请好友得会员"),
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
+ }, "🎁 带新用户换会员"),
2465
3076
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: function () { setView("feedback"); } }, "💬 用户反馈"),
2466
3077
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: togglePwdForm }, pwdOpen ? "收起修改密码" : "🔒 修改密码"),
2467
3078
  h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { doLogout(false); } }, "退出登录")
2468
3079
  ),
3080
+ // 🎁 邀请收益引导气泡:紧贴上面那排按钮(不遮挡按钮、窄屏独占一行)
3081
+ renderInviteTip(),
2469
3082
  h("div", { className: "dru-hint", style: { marginTop: 8 } },
2470
3083
  "升级/续费以带登录态方式打开:点击后生成一次性访问链接并直接跳转,无需重新登录。" +
2471
3084
  (endsAt && isMember ? "到期后如需继续使用会员权益,请在到期前续费。" : "")),
2472
- // 额度将尽 → 轻量邀请入口(文字链层级,不抢「升级 PRO」主 CTA)
2473
- quotaTight
2474
- ? h("div", { className: "dru-hint", style: { marginTop: 6 } },
2475
- "免费额度快用完了?也可以先",
2476
- renderInviteLink("🎯 邀请好友得会员", "邀请好友获得 PRO 会员奖励"),
2477
- "——每邀请满 " + invRuleN + " 位好友,得 " + invRuleDays + " 天 PRO 会员(详情见邀请页)。")
2478
- : null,
3085
+ // 额度将尽 / 已被限速 → 🔀 双路块:升级套餐(转化)or 带新用户换会员时长(拉新)
3086
+ quotaTight ? renderDualPath("quota") : null,
2479
3087
  pwdOpen ? renderResetPwdForm(false) : null
2480
3088
  );
2481
3089
  }
@@ -2488,7 +3096,7 @@ window.__ModuleLoader__.load({
2488
3096
  // 浏览器端不再出现明文手机号。登录卡「忘记密码」为用户自行输入,原样使用。
2489
3097
  var curPhone = ((st && st.config && st.config.phone) || "");
2490
3098
  var ph = fromLogin ? phone : "";
2491
- return h("div", { className: "dru-card", style: { marginTop: 10, border: "1px dashed #d0d7de" } },
3099
+ return h("div", { className: "dru-card", style: { marginTop: 10, border: "1px dashed var(--dru-border)" } },
2492
3100
  h("h3", null, fromLogin ? "忘记密码(短信重置)" : "🔒 修改密码(短信验证)"),
2493
3101
  h("div", { className: "dru-hint", style: { marginBottom: 8 } },
2494
3102
  fromLogin
@@ -2520,7 +3128,7 @@ window.__ModuleLoader__.load({
2520
3128
  );
2521
3129
  }
2522
3130
 
2523
- // ---------- 🎯 邀请视图(0.6.9 重排:奖励大字 → 进度 → 邀请码/链接一键复制 → 折叠规则 → 记录) ----------
3131
+ // ---------- 🎁 邀请视图(0.6.9 重排:交换大字 → 三步走 → 进度 → 邀请码/链接一键复制 → 折叠规则 → 记录) ----------
2524
3132
  /** 体面空态:图标 + 标题 + 说明 + 可选操作(替代过去"一行灰字了事")。 */
2525
3133
  function emptyState(icon, title, sub, actions) {
2526
3134
  return h("div", { className: "dru-empty" },
@@ -2571,13 +3179,13 @@ window.__ModuleLoader__.load({
2571
3179
  : null
2572
3180
  );
2573
3181
  }
2574
- /** 邀请记录里的好友展示名:只用接口给的 invitee_phone(掩码),缺字段时降级,不编造。 */
3182
+ /** 邀请记录里的新用户展示名:只用接口给的 invitee_phone(掩码),缺字段时降级,不编造。 */
2575
3183
  function inviteeName(r) {
2576
3184
  var phone = maskPhoneLike(r && r.invitee_phone);
2577
3185
  if (phone) return phone;
2578
3186
  var dev = r && r.device_id ? String(r.device_id) : "";
2579
- if (dev) return "好友(设备 " + (dev.length > 6 ? dev.slice(-6) : dev) + ")";
2580
- return "好友";
3187
+ if (dev) return "新用户(设备 " + (dev.length > 6 ? dev.slice(-6) : dev) + ")";
3188
+ return "新用户";
2581
3189
  }
2582
3190
  /** 一条邀请记录:谁 / 什么时候 / 是否已生效(时间或状态字段缺失时如实标注,不编造)。 */
2583
3191
  function renderInviteRecords(records, ruleN) {
@@ -2594,8 +3202,11 @@ window.__ModuleLoader__.load({
2594
3202
  ),
2595
3203
  h("div", { className: "dru-rec-sub" },
2596
3204
  pending
2597
- ? "好友已完成设备绑定,奖励按规则结算(每满 " + ruleN + " 位发放一次)。"
2598
- : "好友已完成电脑端安装并绑定设备,已计入你的有效邀请。")
3205
+ ? (ruleN > 0
3206
+ ? "这位新用户已完成设备绑定,奖励按规则结算(每满 " + ruleN + " 位发放一次)。"
3207
+ // 拉新关闭态:不引用具体结算规则(不能显示「每满 0 位」这种假承诺)
3208
+ : "这位新用户已完成设备绑定;奖励结算以运营规则为准(活动当前未开放)。")
3209
+ : "这位新用户已完成电脑端安装并上线,已计入你的有效邀请。")
2599
3210
  );
2600
3211
  }));
2601
3212
  }
@@ -2617,6 +3228,35 @@ window.__ModuleLoader__.load({
2617
3228
  }))
2618
3229
  );
2619
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
+ }
2620
3260
  /** 活动规则:默认折叠,点开才显示(层级清晰,不再和奖励说明挤在一堆小字里)。 */
2621
3261
  function renderInviteRules(n, days) {
2622
3262
  return h("div", { className: "dru-card" },
@@ -2631,11 +3271,13 @@ window.__ModuleLoader__.load({
2631
3271
  ),
2632
3272
  inviteRulesOpen
2633
3273
  ? h("ol", { className: "dru-rules" }, [
2634
- h("li", null, "好友通过你的邀请链接注册(或在注册时填写你的邀请码),即与你绑定邀请关系。"),
2635
- h("li", null, "好友在这台电脑上装好后台服务并成功登录后,才计入 1 位", h("b", null, "有效邀请"), ";同一位好友只计一次。"),
2636
- h("li", null, "每累计 ", h("b", null, n + " 位"), "有效邀请,自动发放 ", h("b", null, days + " 天 PRO 会员"), ",在原到期时间上顺延(已是长期会员则保持长期)。"),
2637
- h("li", null, "奖励自动到账,无需申请:可在「我的邀请记录」下方看到「已到账奖励」。"),
2638
- h("li", null, "邀请码与邀请链接长期有效,随时可在本页复制;好友解绑或重装设备不会撤回已计入的邀请。"),
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, "邀请码与邀请链接长期有效,随时可在本页复制;新用户解绑或重装设备不会撤回已计入的邀请。"),
2639
3281
  h("li", null, "规则与数值以本页面显示的为准(运营可能调整活动力度)。")
2640
3282
  ])
2641
3283
  : null
@@ -2643,9 +3285,9 @@ window.__ModuleLoader__.load({
2643
3285
  }
2644
3286
 
2645
3287
  function renderInvite() {
2646
- var rule = (pub && pub.invite_rule) || {};
2647
- var ruleN = Number(rule.n) > 0 ? Number(rule.n) : 3;
2648
- var ruleDays = Number(rule.days) > 0 ? Number(rule.days) : 15;
3288
+ var inv = inviteRuleOf();
3289
+ var ruleN = inv.n;
3290
+ var ruleDays = inv.days;
2649
3291
  var trialDays = Number(pub && pub.trial_days) > 0 ? Number(pub.trial_days) : 0; // 注册送礼(关闭时不下发/为 0)
2650
3292
  var code = (account && account.invite_code) || "";
2651
3293
  var base = (st && st.remoteUrl) || (pub && pub.app_url) || "https://n.risegao.cn:13443/app/";
@@ -2667,21 +3309,53 @@ window.__ModuleLoader__.load({
2667
3309
  });
2668
3310
  };
2669
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
+
2670
3333
  return h("div", null,
2671
3334
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", style: { width: "100%" }, onClick: function () { setView("home"); } }, "← 返回账号"),
2672
3335
 
2673
- // ① 奖励是什么(大字 + 一句人话)
2674
- card("🎯 邀请好友,一起用远程访问", [
3336
+ // ① 交换是什么(大字:带 N 位新用户 → 得 M 天 PRO;一句人话说清条件与「只有你得奖」)
3337
+ card("🎁 带新用户,换会员时长", [
2675
3338
  h("div", { className: "dru-invite-hero" },
2676
- "邀请 ", h("em", null, ruleN + " 位"), "好友 → 你得 ", h("em", null, ruleDays + " 天 PRO 会员")),
3339
+ "带 ", h("em", null, ruleN + " 位"), "新用户 → 得 ", h("em", null, ruleDays + " 天 PRO")),
2677
3340
  h("div", { className: "dru-invite-sub" },
2678
- "好友用你的邀请链接注册,并在这台电脑上装好后台服务后计入;每满 " + ruleN + " 位自动到账,会员天数可累加"
2679
- + (trialDays > 0 ? "。好友自己注册即得 " + trialDays + " 天 PRO 试用,不用邀请也一样。" : "。")),
2680
- // 进度:已邀请 / 已到账 / 还差几位(数据全部来自接口,未登录或未加载时不编造数字)
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
+ // 进度:已带来 / 已到账 / 还差几位(数据全部来自接口,未登录或未加载时不编造数字)
2681
3355
  loggedInSaaS
2682
3356
  ? h("div", { className: "dru-invite-prog" },
2683
3357
  h("div", { className: "dru-invite-prog-top" },
2684
- h("span", null, "已邀请 ", h("span", { className: "n" }, String(validCount)), " 位好友"),
3358
+ h("span", null, "已带来 ", h("span", { className: "n" }, String(validCount)), " 位新用户"),
2685
3359
  inviteErr ? null : h("span", null, "已到账 " + rewards.length + " 次(每次 " + ruleDays + " 天)")
2686
3360
  ),
2687
3361
  h("div", { className: "dru-invite-bar", role: "img", "aria-label": "距离下一次奖励的进度" },
@@ -2689,19 +3363,19 @@ window.__ModuleLoader__.load({
2689
3363
  h("div", { className: "dru-invite-next" },
2690
3364
  inviteLoading
2691
3365
  ? "正在读取你的邀请记录…"
2692
- : "本轮已累计 " + toward + "/" + ruleN + " 位,再邀请 " + nextGap + " 位即可获得下一次 " + ruleDays + " 天 PRO 会员。")
3366
+ : "本轮已累计 " + toward + "/" + ruleN + " 位,再带 " + nextGap + " 位新用户即可获得下一次 " + ruleDays + " 天 PRO。")
2693
3367
  )
2694
3368
  : null,
2695
3369
  // ② 我的邀请码 / 邀请链接(一键复制)
2696
3370
  code
2697
3371
  ? h("div", null,
2698
- copyRow("我的邀请码", code, "code", "好友注册时填这个码,或直接用下面的邀请链接(自动带上)。"),
2699
- copyRow("我的邀请链接", link, "link", "链接较长,可一键复制后发给好友;好友打开即进入注册页。")
3372
+ copyRow("我的邀请码", code, "code", "新用户注册时填这个码,或直接用下面的邀请链接(自动带上)。"),
3373
+ copyRow("我的邀请链接", link, "link", "链接较长,可一键复制后发给还没注册过的新用户;对方打开即进入注册页。")
2700
3374
  )
2701
3375
  : loggedInSaaS
2702
3376
  ? h("div", { style: { marginTop: 10 } },
2703
3377
  emptyState("🎫", "正在为你生成专属邀请码",
2704
- "邀请码绑定你的手机号账号:好友通过它注册,奖励会自动记到这个账号上。(偶尔需要手动获取一次)",
3378
+ "邀请码绑定你的手机号账号:新用户通过它注册后,奖励会自动记到这个账号上。(偶尔需要手动获取一次)",
2705
3379
  [h("button", {
2706
3380
  type: "button",
2707
3381
  className: "dru-btn dru-btn-primary",
@@ -2718,35 +3392,14 @@ window.__ModuleLoader__.load({
2718
3392
  )
2719
3393
  : h("div", { style: { marginTop: 10 } },
2720
3394
  emptyState("🔑", "登录后即可获得你的专属邀请码",
2721
- "邀请码绑定你的手机号账号:好友通过它注册,奖励会自动记到这个账号上。登录后回到本页即可复制分享。",
3395
+ "邀请码绑定你的手机号账号:新用户通过它注册后,奖励会自动记到这个账号上。登录后回到本页即可复制分享。",
2722
3396
  [h("button", { type: "button", className: "dru-btn dru-btn-primary", onClick: function () { setView("home"); } }, "去登录")])
2723
3397
  )
2724
3398
  ]),
2725
3399
 
2726
3400
  // ③ 邀请记录(谁 / 什么时候 / 是否已生效)
2727
3401
  loggedInSaaS
2728
- ? card("我的邀请记录" + (inviteErr ? "" : "(" + validCount + ")"), [
2729
- inviteLoading
2730
- ? h("div", { className: "dru-invite-sub" }, "正在读取邀请记录…")
2731
- : inviteErr
2732
- ? h("div", null,
2733
- h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 0 } }, "读取邀请记录失败:" + inviteErr),
2734
- h("div", { className: "dru-actions", style: { marginTop: 8 } },
2735
- h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: inviteLoading, onClick: loadInvite }, "重试")))
2736
- : validCount === 0
2737
- ? emptyState("📨", "还没有好友通过邀请安装",
2738
- "把上面的邀请链接发给好友:好友注册并在电脑上装好后台服务后,会自动出现在这里。")
2739
- : h("div", null, [
2740
- h("div", { className: "dru-rec-head" },
2741
- h("span", { style: { flex: "1" } }, "好友"),
2742
- h("span", null, "状态"),
2743
- h("span", null, "时间")),
2744
- renderInviteRecords(records, ruleN),
2745
- renderInviteRewards(rewards, ruleDays),
2746
- h("div", { className: "dru-invite-sub", style: { marginTop: 6 } },
2747
- "名单里的好友都已完成电脑端安装与设备绑定,所以都算有效邀请。")
2748
- ])
2749
- ])
3402
+ ? renderInviteRecordCard(records, rewards, validCount, ruleN, ruleDays, inviteErr, inviteLoading)
2750
3403
  : null,
2751
3404
 
2752
3405
  // ④ 活动规则(默认折叠)
@@ -2831,14 +3484,15 @@ window.__ModuleLoader__.load({
2831
3484
  "首次安装会自动下载并配置,期间请不要关闭 DeepSeek;装完会自动启动后台服务,无需任何操作。"));
2832
3485
  }
2833
3486
  if (connInfo.deviceId) rows.push(h("div", { className: "dru-meta" }, "设备 ID:" + connInfo.deviceId));
2834
- // 【0.6.9】连接明显偏慢时的轻量邀请入口(不打断、不弹窗):
3487
+ // 【0.6.9】连接明显偏慢时的 🔀 双路块(不打断、不弹窗):把「等」变成一次选择 ——
3488
+ // 升级带宽(转化)or 带新用户换会员时长(拉新,会员同样走这条)。
2835
3489
  // 判定只用既有字段 —— 服务端重试次数 attempts≥3、或阶段文案里出现限速/带宽/较慢类提示、
2836
3490
  // 或本地观察「非 online 阶段持续 ≥90s」。判断不了就不显示(宁可不出现,也不误报)。
3491
+ // 放在「立即重试 / 复制诊断信息」之上:先给选择,再给重试(重试仍在,不是死胡同)。
3492
+ // 拉新关闭 + 已是 Pro Max 时 renderDualPath 返回 null → 这块自然不出现(不会留空壳)。
2837
3493
  if (connSlow) {
2838
- rows.push(h("div", { className: "dru-hint", style: { marginTop: 8 } },
2839
- "连接比平时慢一些(仍在自动推进,无需操作):等待时也可以",
2840
- renderInviteLink("🎯 邀请好友得会员", "连接偏慢时顺手邀请好友 —— 每满若干位得 PRO 会员奖励"),
2841
- ",好友装好后双方访问都会更快。"));
3494
+ var dualSlow = renderDualPath("slow");
3495
+ if (dualSlow) rows.push(dualSlow);
2842
3496
  }
2843
3497
  rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
2844
3498
  h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: retryConnect }, busy === "connect-retry" ? "立即重试中…" : "立即重试"),
@@ -2958,7 +3612,7 @@ window.__ModuleLoader__.load({
2958
3612
  h("button", {
2959
3613
  type: "button",
2960
3614
  className: "dru-btn dru-btn-ghost",
2961
- style: { color: "#cf222e", borderColor: "#cf222e" },
3615
+ style: { color: "var(--dru-danger)", borderColor: "var(--dru-danger)" },
2962
3616
  disabled: devBusy !== "",
2963
3617
  onClick: function () { doDeleteDevice(id); }
2964
3618
  }, devBusy === "delete:" + id ? "删除中…" : armedDel === id ? "⚠ 再点一次确认删除记录" : "删除记录")
@@ -2967,7 +3621,7 @@ window.__ModuleLoader__.load({
2967
3621
  }),
2968
3622
  h("div", { className: "dru-hint", style: { marginTop: 4 } },
2969
3623
  "取消配对后,对方需重新扫码/登录才能再次远程访问本机。"),
2970
- h("div", { className: "dru-actions", style: { marginTop: 8, borderTop: "1px dashed #eaeef2", paddingTop: 8 } },
3624
+ h("div", { className: "dru-actions", style: { marginTop: 8, borderTop: "1px dashed var(--dru-border-soft)", paddingTop: 8 } },
2971
3625
  h("button", {
2972
3626
  type: "button",
2973
3627
  className: "dru-btn dru-btn-ghost",
@@ -3050,7 +3704,7 @@ window.__ModuleLoader__.load({
3050
3704
  field("手机号", input({ type: "tel", value: rphone, placeholder: "11 位手机号", autoComplete: "tel", onChange: function (e) { setRphone(e.target.value); } })),
3051
3705
  field("密码", input({ type: "password", value: rpass, placeholder: "至少 8 位", autoComplete: "new-password", onChange: function (e) { setRpass(e.target.value); } })),
3052
3706
  field("确认密码", input({ type: "password", value: rpass2, placeholder: "再次输入密码", autoComplete: "new-password", onChange: function (e) { setRpass2(e.target.value); } })),
3053
- field("邀请码(选填)", input({ value: rInvite, placeholder: "好友的邀请码,如 A8K2M4XQ", autoComplete: "off", onChange: function (e) { setRInvite(e.target.value); } })),
3707
+ field("邀请码(选填)", input({ value: rInvite, placeholder: "邀请你的人的邀请码,如 A8K2M4XQ", autoComplete: "off", onChange: function (e) { setRInvite(e.target.value); } })),
3054
3708
  rcap && field("图形验证码", h("div", { className: "dru-captcha" },
3055
3709
  input({ value: rcapTxt, placeholder: "图中数字", autoComplete: "off", inputMode: "numeric", maxLength: 6, onChange: function (e) { setRcapTxt(e.target.value); } }),
3056
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 },
@@ -3191,7 +3845,7 @@ window.__ModuleLoader__.load({
3191
3845
  function renderCommunityModal() {
3192
3846
  if (!commOpen || !(community && community.qrcode)) return null;
3193
3847
  var close = function () { setCommOpen(false); };
3194
- 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(); } },
3195
3849
  h("div", { className: "dru-popup-card", role: "dialog", "aria-label": "加入企微交流群" },
3196
3850
  h("div", { className: "dru-popup-body" },
3197
3851
  h("div", { className: "dru-popup-icon" }, "💬"),
@@ -3210,7 +3864,7 @@ window.__ModuleLoader__.load({
3210
3864
  );
3211
3865
  }
3212
3866
 
3213
- 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 },
3214
3868
  h("div", { className: "dru-settings-head" },
3215
3869
  h("span", { className: "dru-settings-icon" }, "📱"),
3216
3870
  h("div", null,
@@ -3307,6 +3961,10 @@ window.__ModuleLoader__.load({
3307
3961
  dotWatch();
3308
3962
  // 侧栏「远程访问」快捷入口(2026-09 恢复注入:与官方「设置」按钮共存不遮挡)
3309
3963
  navEnsureStart();
3964
+ // 🎨 主题:**不在这里**建 observer —— 观察器只在真有订阅者时才创建
3965
+ // (面板/弹窗挂载 → themeSubscribe;侧栏入口挂上 → mountNavEntry)。
3966
+ // 这样「没有任何东西需要跟随主题」时不会白留一个 MutationObserver,
3967
+ // 也避免与本插件已有的红点观察器互相干扰。
3310
3968
 
3311
3969
  // 满意度弹窗已停用(不再调度);组件与调度代码保留,便于日后恢复。
3312
3970
  var FB_POPUP_RETIRED = true;