@having5548/dsh-downloader 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/lib/client.js +1343 -650
- package/lib/core/upstream.js +11 -0
- package/package.json +5 -1
- package/scripts/fix-nested-links.mjs +130 -0
package/lib/client.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
|
-
// dsh-downloader —— DSH
|
|
2
|
+
// dsh-downloader —— DSH 下载代理插件(客户端半部)
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
// 1.
|
|
6
|
-
// 2.
|
|
7
|
-
//
|
|
4
|
+
// 两个入口,共用同一套组件与样式(要求「两个入口的 UI 风格一致」):
|
|
5
|
+
// 1. 左侧栏导航页 —— sidebar.panellist(图标行)+ main(keyed 页面)
|
|
6
|
+
// 2. 设置页 —— settings.section(同一套 Page 组件,variant='settings')
|
|
7
|
+
// 另外保留 sidebar.footer.action 的实时流量徽标。
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// 文案:LOCALE_ZH / LOCALE_EN 两份完整字典,经 ctx.locale 注册(DSH 会按系统语言
|
|
10
|
+
// 自动选中,用户也能在 设置 → 语言 里改)。页内另有 自动/中/EN 三态切换,只影响
|
|
11
|
+
// 本插件页面,不劫持全局语言。
|
|
12
|
+
//
|
|
13
|
+
// 必须惰性走 ctx.configForms;scope.subscribe / getSnapshot 是类原型方法,
|
|
12
14
|
// 直接传给 useSyncExternalStore 会因 this 丢失而崩,必须包一层闭包。
|
|
13
15
|
// ============================================================================
|
|
14
16
|
window.__ModuleLoader__.load({
|
|
@@ -26,6 +28,9 @@ window.__ModuleLoader__.load({
|
|
|
26
28
|
var ENTRY_ID = 'dsh-downloader'
|
|
27
29
|
var LOCALE_NS = 'dsh-downloader'
|
|
28
30
|
var API_BASE = '/dsh-downloader/'
|
|
31
|
+
var PANEL_ID = 'dsh-downloader'
|
|
32
|
+
var LS_ADVANCED = 'dsh-downloader.advanced'
|
|
33
|
+
var LS_LANG = 'dsh-downloader.lang'
|
|
29
34
|
|
|
30
35
|
var DEFAULTS = {
|
|
31
36
|
enabled: true,
|
|
@@ -50,308 +55,817 @@ window.__ModuleLoader__.load({
|
|
|
50
55
|
threads: 4,
|
|
51
56
|
insecureTls: false,
|
|
52
57
|
allowOutsideWorkspace: false,
|
|
53
|
-
maxRedirects: 10
|
|
58
|
+
maxRedirects: 10,
|
|
59
|
+
harnessProxy: false,
|
|
60
|
+
harnessProxyPort: 17890
|
|
54
61
|
}
|
|
55
62
|
|
|
63
|
+
// ------------------------------------------------------------------
|
|
64
|
+
// 文案
|
|
65
|
+
// ------------------------------------------------------------------
|
|
56
66
|
var LOCALE_ZH = {
|
|
57
67
|
nav: '下载代理',
|
|
58
68
|
title: '下载代理',
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
updateSubscription: '更新订阅',
|
|
67
|
-
testAll: '全部测速',
|
|
68
|
-
select: '选择',
|
|
69
|
-
test: '测速',
|
|
70
|
-
clearLatency: '清空延迟',
|
|
71
|
-
sessionGuard: '会话保护',
|
|
72
|
-
guardIntro: '把国内 AI 平台域名写进 $DSH_HOME/.env 的 NO_PROXY,让模型连接绕开全局代理 —— 代理程序关掉或节点抖动都不会断链。写入前会自动备份。',
|
|
73
|
-
guardApply: '写入 NO_PROXY',
|
|
74
|
-
guardRestore: '还原备份',
|
|
75
|
-
guardCovered: '已覆盖',
|
|
76
|
-
guardMissing: '缺少',
|
|
77
|
-
guardNoProxyVars: '该文件没有配置 HTTP_PROXY / HTTPS_PROXY,此项暂时无效',
|
|
78
|
-
guardRestart: '改完需要重启 DSH 才生效',
|
|
79
|
-
notRunning: '未运行',
|
|
69
|
+
subtitle: '境外文件走代理、境内直连,由插件自带的订阅内核提供出口',
|
|
70
|
+
entryLabel: '下载代理',
|
|
71
|
+
langAuto: '自动',
|
|
72
|
+
langZh: '中文',
|
|
73
|
+
langEn: 'English',
|
|
74
|
+
|
|
75
|
+
// 状态
|
|
80
76
|
stRunning: '运行中',
|
|
81
77
|
stDisabled: '已关闭',
|
|
82
|
-
stNoSubscription: '无可用订阅',
|
|
83
78
|
stFailed: '失败',
|
|
84
79
|
stIdle: '未启动',
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
stNoSubscription: '未配置订阅',
|
|
81
|
+
stUnavailable: '不可用',
|
|
82
|
+
refresh: '刷新',
|
|
83
|
+
restartCore: '重启内核',
|
|
84
|
+
corePort: '内核端口',
|
|
85
|
+
coreNone: '未运行',
|
|
86
|
+
nodesCount: '节点',
|
|
87
|
+
currentNode: '当前节点',
|
|
88
|
+
nodeAuto: '自动选择',
|
|
89
|
+
modeExplicit: '固定代理地址',
|
|
90
|
+
modeSubscription: '订阅',
|
|
91
|
+
modeEnvironment: '环境代理',
|
|
92
|
+
modeNone: '未配置',
|
|
93
|
+
upstream: '上游',
|
|
94
|
+
upstreamNone: '既没有配置订阅,也没有代理地址',
|
|
95
|
+
notRunningHint: '内核没有运行,下载会直接失败。先看下面的上游配置。',
|
|
96
|
+
|
|
97
|
+
// 配置(节点配置)
|
|
90
98
|
profiles: '节点配置',
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
profilesHint: '一份配置 = 一条订阅地址或一个导入的配置文件',
|
|
100
|
+
addProfile: '添加配置',
|
|
101
|
+
importUrl: '订阅地址',
|
|
93
102
|
importUrlPlaceholder: 'https://… 或 clash://install-config?url=…',
|
|
94
|
-
|
|
95
|
-
importFile: '
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
importLabel: '名称(可留空)',
|
|
104
|
+
importFile: '导入文件',
|
|
105
|
+
import: '导入',
|
|
106
|
+
cancel: '取消',
|
|
107
|
+
emptyProfiles: '还没有配置。点右下角「添加配置」导入订阅或文件。',
|
|
108
|
+
current: '当前',
|
|
109
|
+
fileSource: '本地文件',
|
|
110
|
+
updatedAt: '更新于',
|
|
111
|
+
never: '从未更新',
|
|
112
|
+
use: '选用',
|
|
113
|
+
update: '更新',
|
|
114
|
+
remove: '删除',
|
|
115
|
+
autoUpdate: '自动更新',
|
|
116
|
+
autoUpdateOn: '自动更新 · 每 %s 小时',
|
|
117
|
+
trafficQuota: '流量',
|
|
118
|
+
|
|
119
|
+
// 节点
|
|
120
|
+
nodes: '代理节点',
|
|
121
|
+
nodesHint: '点一个节点即选用;「自动选择」按延迟自动挑',
|
|
122
|
+
testAll: '全部测速',
|
|
123
|
+
testing: '测速中…',
|
|
124
|
+
ms: '%s ms',
|
|
125
|
+
timeout: '超时',
|
|
126
|
+
untested: '未测',
|
|
127
|
+
needsNative: '需连接器',
|
|
128
|
+
nodeType: '类型',
|
|
129
|
+
|
|
130
|
+
// 下载
|
|
131
|
+
download: '下载',
|
|
132
|
+
downloadDir: '下载目录',
|
|
133
|
+
downloadDirHint: '留空 = DSH 的 downloads 目录',
|
|
134
|
+
threads: '线程数',
|
|
135
|
+
threadsHint: '服务端支持时按此并发下载;1 = 单连接',
|
|
136
|
+
maxMb: '单文件上限(MB)',
|
|
137
|
+
dirPlaceholder: '留空使用默认目录',
|
|
138
|
+
|
|
139
|
+
// 高级
|
|
140
|
+
advanced: '高级设置',
|
|
141
|
+
advancedHint: '打开后显示全部可调项',
|
|
142
|
+
advUpstream: '上游与订阅',
|
|
143
|
+
proxyUrl: '固定代理地址',
|
|
144
|
+
proxyUrlHint: '显式上游,支持 http:// 与 socks5://;填了就不再使用订阅',
|
|
145
|
+
subscriptionUrl: '订阅地址(旧版)',
|
|
146
|
+
subscriptionUrlHint: '只有在没有任何「节点配置」时才会用它',
|
|
147
|
+
fetchProxyUrl: '拉取订阅用的代理',
|
|
148
|
+
fetchProxyUrlHint: '仅当订阅本身被墙时填,必须是 http(s):// 代理',
|
|
149
|
+
groupType: '分组类型',
|
|
150
|
+
preferredNode: '首选节点',
|
|
151
|
+
preferredNodeHint: '按名字固定一个节点,优先级最高',
|
|
152
|
+
autoUpdateHours: '订阅自动更新(小时)',
|
|
153
|
+
latencyTestUrl: '测速地址',
|
|
154
|
+
latencyTimeoutMs: '测速超时(ms)',
|
|
155
|
+
advRouting: '分流',
|
|
156
|
+
domesticDirect: '境内直连',
|
|
157
|
+
domesticDirectHint: '关闭后所有目标都走代理',
|
|
158
|
+
protectAiPlatforms: '国内 AI 平台强制直连',
|
|
159
|
+
protectAiPlatformsHint: '最高优先级 DIRECT,连代理抖动也不会断会话',
|
|
160
|
+
extraDirectDomains: '额外直连域名',
|
|
161
|
+
extraDirectDomainsHint: '逗号或换行分隔',
|
|
162
|
+
extraRules: '追加规则',
|
|
163
|
+
extraRulesHint: '每行一条,如 DOMAIN-SUFFIX,example.com,DIRECT',
|
|
164
|
+
excludeRules: '剔除规则',
|
|
165
|
+
excludeRulesHint: '每行一条,子串匹配订阅规则',
|
|
166
|
+
advEngine: '下载引擎',
|
|
167
|
+
maxRedirects: '最大重定向次数',
|
|
168
|
+
stallTimeoutS: '停滞判定(秒)',
|
|
169
|
+
downloadTimeoutS: '总时长预算(秒)',
|
|
170
|
+
insecureTls: '跳过 TLS 校验',
|
|
171
|
+
insecureTlsHint: '仅企业 TLS 解密环境需要',
|
|
172
|
+
allowOutsideWorkspace: '允许写工作区外',
|
|
173
|
+
allowOutsideWorkspaceHint: '否则只写到工作区与下载目录',
|
|
174
|
+
guardShellDownloads: '拦截未走代理的境外 shell 下载',
|
|
175
|
+
guardShellDownloadsHint: '拦到就改用 dsh_run_proxied 重跑',
|
|
176
|
+
advHarness: 'harness 出网',
|
|
177
|
+
harnessProxy: '开放给 harness',
|
|
178
|
+
harnessProxyHint: '让 web_fetch / shell 子进程 / MCP 的 HTTP 也走内核。写入 $DSH_HOME/.env 后需重启 DSH',
|
|
179
|
+
harnessProxyPort: '固定端口',
|
|
180
|
+
harnessProxyPortHint: '写进 .env,不能随机;被占用时自动退回随机端口',
|
|
181
|
+
|
|
182
|
+
// 会话保护
|
|
183
|
+
guard: '会话保护',
|
|
184
|
+
guardHint: '把国内 AI 平台域名写进 NO_PROXY,代理关掉也不断会话',
|
|
185
|
+
guardCovered: '已覆盖 %s 个域名',
|
|
186
|
+
guardMissing: '缺少 %s 个',
|
|
187
|
+
guardApply: '写入',
|
|
188
|
+
guardRestore: '还原',
|
|
189
|
+
guardNoVars: '该文件还没有配置 HTTP_PROXY,这项暂时不起作用',
|
|
190
|
+
guardRestart: '改完需要重启 DSH',
|
|
191
|
+
|
|
192
|
+
// 流量 / 页脚
|
|
193
|
+
traffic: '流量',
|
|
107
194
|
trafficUp: '上行',
|
|
108
195
|
trafficDown: '下行',
|
|
109
196
|
trafficTotal: '累计',
|
|
110
|
-
|
|
197
|
+
trafficIdle: '内核没在跑,没有流量',
|
|
198
|
+
|
|
199
|
+
// 其它
|
|
200
|
+
unavailable: '当前环境无法持久化设置,本次修改仅当前页面生效。',
|
|
201
|
+
scopeNote: '内核只绑 127.0.0.1 并要求本进程令牌,其它程序连不上也借不走。',
|
|
202
|
+
about: '关于',
|
|
203
|
+
dataDir: '数据目录',
|
|
204
|
+
saved: '已保存',
|
|
205
|
+
failed: '失败'
|
|
111
206
|
}
|
|
207
|
+
|
|
112
208
|
var LOCALE_EN = {
|
|
113
209
|
nav: 'Download proxy',
|
|
114
210
|
title: 'Download proxy',
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
211
|
+
subtitle: 'Foreign files go through the proxy, domestic stay direct — the exit comes from the plugin own subscription core',
|
|
212
|
+
entryLabel: 'Download proxy',
|
|
213
|
+
langAuto: 'Auto',
|
|
214
|
+
langZh: '中文',
|
|
215
|
+
langEn: 'English',
|
|
216
|
+
|
|
217
|
+
stRunning: 'Running',
|
|
218
|
+
stDisabled: 'Disabled',
|
|
219
|
+
stFailed: 'Failed',
|
|
220
|
+
stIdle: 'Not started',
|
|
221
|
+
stNoSubscription: 'No subscription',
|
|
222
|
+
stUnavailable: 'Unavailable',
|
|
120
223
|
refresh: 'Refresh',
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
notRunning: 'not running',
|
|
136
|
-
stRunning: 'running',
|
|
137
|
-
stDisabled: 'disabled',
|
|
138
|
-
stNoSubscription: 'no usable subscription',
|
|
139
|
-
stFailed: 'failed',
|
|
140
|
-
stIdle: 'idle',
|
|
141
|
-
mdExplicit: 'upstream: configured proxyUrl',
|
|
142
|
-
mdEnvironment: 'upstream: launch environment',
|
|
143
|
-
mdSubscription: 'upstream: self-contained subscription',
|
|
144
|
-
mdNone: 'upstream: not configured',
|
|
145
|
-
nodesBadge: 'nodes',
|
|
224
|
+
restartCore: 'Restart core',
|
|
225
|
+
corePort: 'Core port',
|
|
226
|
+
coreNone: 'not running',
|
|
227
|
+
nodesCount: 'Nodes',
|
|
228
|
+
currentNode: 'Current node',
|
|
229
|
+
nodeAuto: 'Auto select',
|
|
230
|
+
modeExplicit: 'Fixed proxy URL',
|
|
231
|
+
modeSubscription: 'Subscription',
|
|
232
|
+
modeEnvironment: 'Environment proxy',
|
|
233
|
+
modeNone: 'Not configured',
|
|
234
|
+
upstream: 'Upstream',
|
|
235
|
+
upstreamNone: 'Neither a subscription nor a proxy URL is configured',
|
|
236
|
+
notRunningHint: 'The core is not running, so downloads will fail. Check the upstream settings below.',
|
|
237
|
+
|
|
146
238
|
profiles: 'Node profiles',
|
|
147
|
-
|
|
148
|
-
|
|
239
|
+
profilesHint: 'One profile = a subscription URL or an imported config file',
|
|
240
|
+
addProfile: 'Add profile',
|
|
241
|
+
importUrl: 'Subscription URL',
|
|
149
242
|
importUrlPlaceholder: 'https://… or clash://install-config?url=…',
|
|
150
|
-
|
|
151
|
-
importFile: 'Import
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
243
|
+
importLabel: 'Label (optional)',
|
|
244
|
+
importFile: 'Import file',
|
|
245
|
+
import: 'Import',
|
|
246
|
+
cancel: 'Cancel',
|
|
247
|
+
emptyProfiles: 'No profiles yet. Use "Add profile" in the corner to import one.',
|
|
248
|
+
current: 'Current',
|
|
249
|
+
fileSource: 'Local file',
|
|
250
|
+
updatedAt: 'Updated',
|
|
251
|
+
never: 'never',
|
|
252
|
+
use: 'Use',
|
|
253
|
+
update: 'Update',
|
|
254
|
+
remove: 'Remove',
|
|
255
|
+
autoUpdate: 'Auto update',
|
|
256
|
+
autoUpdateOn: 'Auto update · every %s h',
|
|
257
|
+
trafficQuota: 'Traffic',
|
|
258
|
+
|
|
259
|
+
nodes: 'Nodes',
|
|
260
|
+
nodesHint: 'Click a node to use it; "Auto select" picks by latency',
|
|
261
|
+
testAll: 'Test all',
|
|
262
|
+
testing: 'Testing…',
|
|
263
|
+
ms: '%s ms',
|
|
264
|
+
timeout: 'Timeout',
|
|
265
|
+
untested: 'untested',
|
|
266
|
+
needsNative: 'needs connector',
|
|
267
|
+
nodeType: 'Type',
|
|
268
|
+
|
|
269
|
+
download: 'Download',
|
|
270
|
+
downloadDir: 'Download folder',
|
|
271
|
+
downloadDirHint: 'Empty = the DSH downloads folder',
|
|
272
|
+
threads: 'Threads',
|
|
273
|
+
threadsHint: 'Concurrent chunks when the server supports ranges; 1 = single stream',
|
|
274
|
+
maxMb: 'Max file size (MB)',
|
|
275
|
+
dirPlaceholder: 'Empty uses the default folder',
|
|
276
|
+
|
|
277
|
+
advanced: 'Advanced',
|
|
278
|
+
advancedHint: 'Reveal every tunable option',
|
|
279
|
+
advUpstream: 'Upstream and subscription',
|
|
280
|
+
proxyUrl: 'Fixed proxy URL',
|
|
281
|
+
proxyUrlHint: 'Explicit upstream, http:// or socks5://; when set, the subscription is unused',
|
|
282
|
+
subscriptionUrl: 'Subscription URL (legacy)',
|
|
283
|
+
subscriptionUrlHint: 'Only used when no "node profile" exists',
|
|
284
|
+
fetchProxyUrl: 'Proxy for fetching the subscription',
|
|
285
|
+
fetchProxyUrlHint: 'Only when the subscription itself is blocked; must be an http(s):// proxy',
|
|
286
|
+
groupType: 'Group type',
|
|
287
|
+
preferredNode: 'Preferred node',
|
|
288
|
+
preferredNodeHint: 'Pin one node by name; highest priority',
|
|
289
|
+
autoUpdateHours: 'Subscription auto update (h)',
|
|
290
|
+
latencyTestUrl: 'Latency test URL',
|
|
291
|
+
latencyTimeoutMs: 'Latency timeout (ms)',
|
|
292
|
+
advRouting: 'Routing',
|
|
293
|
+
domesticDirect: 'Domestic direct',
|
|
294
|
+
domesticDirectHint: 'Turn off to proxy every target',
|
|
295
|
+
protectAiPlatforms: 'Pin domestic AI platforms direct',
|
|
296
|
+
protectAiPlatformsHint: 'Highest priority DIRECT, so a flapping proxy cannot break the session',
|
|
297
|
+
extraDirectDomains: 'Extra direct domains',
|
|
298
|
+
extraDirectDomainsHint: 'Comma or newline separated',
|
|
299
|
+
extraRules: 'Extra rules',
|
|
300
|
+
extraRulesHint: 'One per line, e.g. DOMAIN-SUFFIX,example.com,DIRECT',
|
|
301
|
+
excludeRules: 'Excluded rules',
|
|
302
|
+
excludeRulesHint: 'One per line, substring match against subscription rules',
|
|
303
|
+
advEngine: 'Download engine',
|
|
304
|
+
maxRedirects: 'Max redirects',
|
|
305
|
+
stallTimeoutS: 'Stall timeout (s)',
|
|
306
|
+
downloadTimeoutS: 'Total budget (s)',
|
|
307
|
+
insecureTls: 'Skip TLS verification',
|
|
308
|
+
insecureTlsHint: 'Only for enterprise TLS inspection',
|
|
309
|
+
allowOutsideWorkspace: 'Write outside the workspace',
|
|
310
|
+
allowOutsideWorkspaceHint: 'Otherwise only the workspace and the download folder',
|
|
311
|
+
guardShellDownloads: 'Block unproxied overseas shell downloads',
|
|
312
|
+
guardShellDownloadsHint: 'When blocked, re-run through dsh_run_proxied',
|
|
313
|
+
advHarness: 'Harness egress',
|
|
314
|
+
harnessProxy: 'Expose the core to the harness',
|
|
315
|
+
harnessProxyHint: 'Also route web_fetch / shell children / MCP HTTP. Written to $DSH_HOME/.env; restart DSH',
|
|
316
|
+
harnessProxyPort: 'Fixed port',
|
|
317
|
+
harnessProxyPortHint: 'Must be fixed because .env pins it; falls back to a random port when taken',
|
|
318
|
+
|
|
319
|
+
guard: 'Session guard',
|
|
320
|
+
guardHint: 'Write the AI platform domains into NO_PROXY so a dead proxy cannot drop the session',
|
|
321
|
+
guardCovered: '%s domains covered',
|
|
322
|
+
guardMissing: '%s missing',
|
|
323
|
+
guardApply: 'Write',
|
|
324
|
+
guardRestore: 'Restore',
|
|
325
|
+
guardNoVars: 'That file has no HTTP_PROXY yet, so this has no effect',
|
|
326
|
+
guardRestart: 'Restart DSH to apply',
|
|
327
|
+
|
|
328
|
+
traffic: 'Traffic',
|
|
163
329
|
trafficUp: 'Up',
|
|
164
330
|
trafficDown: 'Down',
|
|
165
331
|
trafficTotal: 'total',
|
|
166
|
-
|
|
332
|
+
trafficIdle: 'The core is not running, so there is no traffic',
|
|
333
|
+
|
|
334
|
+
unavailable: 'Settings cannot be persisted here; changes apply to this page only.',
|
|
335
|
+
scopeNote: 'The core binds 127.0.0.1 and requires this process token, so other programs cannot borrow it.',
|
|
336
|
+
about: 'About',
|
|
337
|
+
dataDir: 'Data folder',
|
|
338
|
+
saved: 'Saved',
|
|
339
|
+
failed: 'failed'
|
|
167
340
|
}
|
|
168
341
|
|
|
169
342
|
// ------------------------------------------------------------------
|
|
170
|
-
//
|
|
343
|
+
// 小工具
|
|
171
344
|
// ------------------------------------------------------------------
|
|
172
345
|
function formatBytes(value) {
|
|
173
|
-
var n = Number(value)
|
|
346
|
+
var n = Number(value)
|
|
347
|
+
if (!isFinite(n) || n <= 0) return '0 B'
|
|
174
348
|
var units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
175
349
|
var i = 0
|
|
176
|
-
while (n >= 1024 && i < units.length - 1) { n
|
|
177
|
-
return (i === 0 ? n : n.toFixed(
|
|
350
|
+
while (n >= 1024 && i < units.length - 1) { n = n / 1024; i += 1 }
|
|
351
|
+
return (n >= 100 || i === 0 ? Math.round(n) : n.toFixed(1)) + ' ' + units[i]
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function formatRate(value) {
|
|
355
|
+
var n = Number(value)
|
|
356
|
+
if (!isFinite(n) || n <= 0) return '0 B/s'
|
|
357
|
+
return formatBytes(n) + '/s'
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function formatRateTiny(value) {
|
|
361
|
+
var n = Number(value)
|
|
362
|
+
if (!isFinite(n) || n <= 0) return '0'
|
|
363
|
+
if (n < 1024) return Math.round(n) + 'B'
|
|
364
|
+
if (n < 1024 * 1024) return Math.round(n / 1024) + 'K'
|
|
365
|
+
return (n / 1024 / 1024).toFixed(1) + 'M'
|
|
178
366
|
}
|
|
179
367
|
|
|
180
|
-
function formatTime(
|
|
181
|
-
if (
|
|
182
|
-
try {
|
|
368
|
+
function formatTime(value) {
|
|
369
|
+
if (!value) return ''
|
|
370
|
+
try {
|
|
371
|
+
var d = new Date(value)
|
|
372
|
+
if (isNaN(d.getTime())) return ''
|
|
373
|
+
var pad = function (n) { return (n < 10 ? '0' : '') + n }
|
|
374
|
+
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes())
|
|
375
|
+
} catch (e) { return '' }
|
|
183
376
|
}
|
|
184
377
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
378
|
+
function readStore(key, fallback) {
|
|
379
|
+
try {
|
|
380
|
+
var raw = window.localStorage.getItem(key)
|
|
381
|
+
return raw === null ? fallback : raw
|
|
382
|
+
} catch (e) { return fallback }
|
|
188
383
|
}
|
|
189
384
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
var n = Number(bytesPerSecond) || 0
|
|
193
|
-
if (n < 1024) return n.toFixed(0) + 'B'
|
|
194
|
-
if (n < 1024 * 1024) return (n / 1024).toFixed(0) + 'K'
|
|
195
|
-
if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + 'M'
|
|
196
|
-
return (n / 1024 / 1024 / 1024).toFixed(1) + 'G'
|
|
385
|
+
function writeStore(key, value) {
|
|
386
|
+
try { window.localStorage.setItem(key, value) } catch (e) { /* ignore */ }
|
|
197
387
|
}
|
|
198
388
|
|
|
199
|
-
/** 与后端同源的请求封装(面板内外都要用,所以放在模块层)。 */
|
|
200
389
|
function apiFetch(path, body) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
headers
|
|
204
|
-
body
|
|
205
|
-
}
|
|
390
|
+
var init = { method: body === undefined ? 'GET' : 'POST' }
|
|
391
|
+
if (body !== undefined) {
|
|
392
|
+
init.headers = { 'content-type': 'application/json' }
|
|
393
|
+
init.body = JSON.stringify(body)
|
|
394
|
+
}
|
|
395
|
+
return fetch(API_BASE + path, init).then(function (response) {
|
|
206
396
|
return response.text().then(function (text) {
|
|
207
|
-
var payload =
|
|
397
|
+
var payload = {}
|
|
398
|
+
if (text.length > 0) {
|
|
399
|
+
try { payload = JSON.parse(text) } catch (e) { payload = { error: text } }
|
|
400
|
+
}
|
|
208
401
|
if (!response.ok) throw new Error(payload.error || ('HTTP ' + response.status))
|
|
209
402
|
return payload
|
|
210
403
|
})
|
|
211
404
|
})
|
|
212
405
|
}
|
|
213
406
|
|
|
214
|
-
function
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
407
|
+
function cls() {
|
|
408
|
+
var parts = []
|
|
409
|
+
for (var i = 0; i < arguments.length; i += 1) {
|
|
410
|
+
if (arguments[i]) parts.push(arguments[i])
|
|
411
|
+
}
|
|
412
|
+
return parts.join(' ')
|
|
219
413
|
}
|
|
220
414
|
|
|
221
|
-
|
|
222
|
-
|
|
415
|
+
/**
|
|
416
|
+
* 取 locale 服务。
|
|
417
|
+
*
|
|
418
|
+
* `ctx.locale` 只在 `inject` 里声明了硬依赖 `locale` 时才存在;本插件只 inject
|
|
419
|
+
* `slots`,所以必须走可选访问 `ctx.get('locale')` —— 否则下面全部降级成英文,
|
|
420
|
+
* 而界面其它地方仍是中文(实测踩过)。
|
|
421
|
+
*/
|
|
422
|
+
function localeService(ctx) {
|
|
423
|
+
if (!ctx) return null
|
|
424
|
+
try {
|
|
425
|
+
if (ctx.locale) return ctx.locale
|
|
426
|
+
} catch (e) { /* ignore */ }
|
|
427
|
+
try {
|
|
428
|
+
var service = ctx.get('locale')
|
|
429
|
+
if (service) return service
|
|
430
|
+
} catch (e) { /* ignore */ }
|
|
431
|
+
return null
|
|
223
432
|
}
|
|
224
433
|
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
434
|
+
// ------------------------------------------------------------------
|
|
435
|
+
// 样式(全部走 DSH 主题令牌,自动适配明暗)
|
|
436
|
+
// ------------------------------------------------------------------
|
|
437
|
+
var CSS_ID = 'dshdl-styles'
|
|
228
438
|
|
|
229
|
-
function
|
|
230
|
-
|
|
231
|
-
|
|
439
|
+
function ensureCss() {
|
|
440
|
+
if (document.getElementById(CSS_ID)) return
|
|
441
|
+
var style = document.createElement('style')
|
|
442
|
+
style.id = CSS_ID
|
|
443
|
+
style.textContent = [
|
|
444
|
+
'.dshdl-root{display:flex;flex-direction:column;gap:22px;padding:22px 26px 40px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;}',
|
|
445
|
+
'.dshdl-root.dshdl-compact{padding:16px 16px 32px;gap:18px;}',
|
|
446
|
+
'.dshdl-head{display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;}',
|
|
447
|
+
'.dshdl-headText{flex:1 1 auto;min-width:180px;}',
|
|
448
|
+
'.dshdl-h1{margin:0;font-size:20px;font-weight:600;letter-spacing:.2px;}',
|
|
449
|
+
'.dshdl-sub{margin:3px 0 0;color:var(--dsw-alias-label-secondary);font-size:12px;}',
|
|
450
|
+
'.dshdl-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}',
|
|
451
|
+
'.dshdl-pill{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:999px;font-size:12px;border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary);}',
|
|
452
|
+
'.dshdl-pill.ok{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary);}',
|
|
453
|
+
'.dshdl-pill.err{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);}',
|
|
454
|
+
'.dshdl-pill.warn{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary);}',
|
|
455
|
+
'.dshdl-dot{width:6px;height:6px;border-radius:50%;background:currentColor;display:inline-block;}',
|
|
456
|
+
'.dshdl-btn{display:inline-flex;align-items:center;gap:6px;padding:5px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px;cursor:pointer;}',
|
|
457
|
+
'.dshdl-btn:hover:not(:disabled){border-color:var(--dsw-alias-border-l2);}',
|
|
458
|
+
'.dshdl-btn:disabled{opacity:.5;cursor:default;}',
|
|
459
|
+
'.dshdl-btn.primary{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);color:#fff;}',
|
|
460
|
+
'.dshdl-btn.ghost{background:transparent;}',
|
|
461
|
+
'.dshdl-btn.danger{color:var(--dsw-alias-state-error-primary);}',
|
|
462
|
+
'.dshdl-seg{display:inline-flex;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;overflow:hidden;}',
|
|
463
|
+
'.dshdl-seg button{padding:4px 10px;font-size:12px;background:transparent;border:0;color:var(--dsw-alias-label-secondary);cursor:pointer;}',
|
|
464
|
+
'.dshdl-seg button.on{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);}',
|
|
465
|
+
'.dshdl-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;padding:16px 18px;}',
|
|
466
|
+
'.dshdl-cardTitle{display:flex;align-items:center;gap:10px;margin:0 0 4px;font-size:14px;font-weight:600;}',
|
|
467
|
+
'.dshdl-cardHint{margin:0 0 14px;color:var(--dsw-alias-label-secondary);font-size:12px;}',
|
|
468
|
+
'.dshdl-kv{display:grid;grid-template-columns:auto 1fr;gap:6px 16px;font-size:12px;}',
|
|
469
|
+
'.dshdl-kv dt{color:var(--dsw-alias-label-secondary);}',
|
|
470
|
+
'.dshdl-kv dd{margin:0;word-break:break-all;}',
|
|
471
|
+
'.dshdl-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(196px,1fr));gap:10px;}',
|
|
472
|
+
'.dshdl-tile{position:relative;display:flex;flex-direction:column;gap:8px;padding:12px 14px;border-radius:10px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);cursor:pointer;transition:border-color .12s ease,transform .12s ease;}',
|
|
473
|
+
'.dshdl-tile:hover{border-color:var(--dsw-alias-border-l2);}',
|
|
474
|
+
'.dshdl-tile.on{border-color:var(--dsw-alias-brand-primary);}',
|
|
475
|
+
'.dshdl-tile.dead{opacity:.62;}',
|
|
476
|
+
'.dshdl-tileName{font-size:13px;font-weight:600;padding-right:22px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
|
|
477
|
+
'.dshdl-tileMeta{font-size:11px;color:var(--dsw-alias-label-secondary);}',
|
|
478
|
+
'.dshdl-tileFoot{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:11px;color:var(--dsw-alias-label-secondary);}',
|
|
479
|
+
'.dshdl-lat{font-variant-numeric:tabular-nums;}',
|
|
480
|
+
'.dshdl-lat.ok{color:var(--dsw-alias-state-success-primary);}',
|
|
481
|
+
'.dshdl-lat.bad{color:var(--dsw-alias-state-error-primary);}',
|
|
482
|
+
'.dshdl-bar{height:3px;border-radius:2px;background:var(--dsw-alias-border-l1);overflow:hidden;}',
|
|
483
|
+
'.dshdl-bar span{display:block;height:100%;background:var(--dsw-alias-brand-primary);}',
|
|
484
|
+
'.dshdl-more{position:absolute;top:8px;right:8px;width:22px;height:22px;border-radius:6px;border:0;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;line-height:1;}',
|
|
485
|
+
'.dshdl-more:hover{background:var(--dsw-alias-bg-layer-1);}',
|
|
486
|
+
'.dshdl-menu{position:absolute;top:32px;right:8px;z-index:5;min-width:132px;padding:4px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-overlay);box-shadow:0 8px 24px rgba(0,0,0,.28);}',
|
|
487
|
+
'.dshdl-menu button{display:block;width:100%;text-align:left;padding:6px 10px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font-size:12px;cursor:pointer;}',
|
|
488
|
+
'.dshdl-menu button:hover{background:var(--dsw-alias-bg-layer-2);}',
|
|
489
|
+
'.dshdl-menu button.danger{color:var(--dsw-alias-state-error-primary);}',
|
|
490
|
+
'.dshdl-row{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:9px 0;border-top:1px solid var(--dsw-alias-border-l1);}',
|
|
491
|
+
'.dshdl-row:first-child{border-top:0;}',
|
|
492
|
+
'.dshdl-rowText{flex:1 1 auto;min-width:0;}',
|
|
493
|
+
'.dshdl-rowLabel{font-size:12.5px;}',
|
|
494
|
+
'.dshdl-rowHint{margin-top:2px;font-size:11px;color:var(--dsw-alias-label-secondary);}',
|
|
495
|
+
'.dshdl-rowCtl{flex:0 0 auto;display:flex;align-items:center;gap:8px;}',
|
|
496
|
+
'.dshdl-input,.dshdl-textarea,.dshdl-select{width:100%;min-width:140px;padding:6px 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-size:12px;font-family:inherit;}',
|
|
497
|
+
'.dshdl-textarea{min-height:58px;resize:vertical;}',
|
|
498
|
+
'.dshdl-input:focus,.dshdl-textarea:focus,.dshdl-select:focus{outline:none;border-color:var(--dsw-alias-brand-primary);}',
|
|
499
|
+
'.dshdl-row.vertical{display:block;}',
|
|
500
|
+
'.dshdl-row.vertical .dshdl-rowCtl{margin-top:6px;}',
|
|
501
|
+
'.dshdl-switch{position:relative;width:36px;height:20px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);cursor:pointer;flex:0 0 auto;}',
|
|
502
|
+
'.dshdl-switch span{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:var(--dsw-alias-label-secondary);transition:transform .14s ease;}',
|
|
503
|
+
'.dshdl-switch.on{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);}',
|
|
504
|
+
'.dshdl-switch.on span{transform:translateX(16px);background:#fff;}',
|
|
505
|
+
'.dshdl-traffic{display:flex;gap:26px;flex-wrap:wrap;}',
|
|
506
|
+
'.dshdl-tItem{min-width:110px;}',
|
|
507
|
+
'.dshdl-tLabel{font-size:11px;color:var(--dsw-alias-label-secondary);}',
|
|
508
|
+
'.dshdl-tValue{font-size:16px;font-weight:600;font-variant-numeric:tabular-nums;}',
|
|
509
|
+
'.dshdl-tSum{font-size:11px;color:var(--dsw-alias-label-secondary);}',
|
|
510
|
+
'.dshdl-inlineForm{display:grid;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l1);}',
|
|
511
|
+
'.dshdl-inlineRow{display:flex;gap:8px;flex-wrap:wrap;}',
|
|
512
|
+
'.dshdl-inlineRow .dshdl-input{flex:1 1 200px;}',
|
|
513
|
+
'.dshdl-note{padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:12px;}',
|
|
514
|
+
'.dshdl-err{padding:8px 12px;border-radius:8px;border:1px solid var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);font-size:12px;}',
|
|
515
|
+
'.dshdl-empty{padding:22px 0;text-align:center;color:var(--dsw-alias-label-secondary);font-size:12px;}',
|
|
516
|
+
'.dshdl-trafFoot{display:flex;flex-direction:column;gap:1px;font-size:11px;line-height:1.25;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;}',
|
|
517
|
+
'.dshdl-trafFoot .row{display:flex;align-items:center;gap:5px;}',
|
|
518
|
+
'.dshdl-trafRail{display:flex;flex-direction:column;align-items:center;font-size:10px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;}'
|
|
519
|
+
].join('\n')
|
|
520
|
+
document.head.appendChild(style)
|
|
232
521
|
}
|
|
233
522
|
|
|
234
523
|
// ------------------------------------------------------------------
|
|
235
|
-
//
|
|
524
|
+
// 基础组件
|
|
236
525
|
// ------------------------------------------------------------------
|
|
237
|
-
|
|
238
|
-
'.dshdl{display:flex;flex-direction:column;gap:16px}',
|
|
239
|
-
'.dshdl-title{font-size:15px;font-weight:600;margin:0}',
|
|
240
|
-
'.dshdl-desc{font-size:12.5px;line-height:19px;color:var(--dsw-alias-label-secondary,#5b616b);margin:0}',
|
|
241
|
-
'.dshdl-card{border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:10px;background:var(--dsw-alias-bg-layer-2,transparent)}',
|
|
242
|
-
'.dshdl-cardHead{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}',
|
|
243
|
-
'.dshdl-cardTitle{font-size:13px;font-weight:600}',
|
|
244
|
-
'.dshdl-badges{display:flex;gap:6px;flex-wrap:wrap}',
|
|
245
|
-
'.dshdl-badge{font-size:11.5px;line-height:16px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.14));color:var(--dsw-alias-label-secondary,#5b616b)}',
|
|
246
|
-
'.dshdl-badge.dshdl-ok{border-color:var(--dsw-alias-state-success-primary,#2fa95e);color:var(--dsw-alias-state-success-primary,#2fa95e)}',
|
|
247
|
-
'.dshdl-badge.dshdl-warn{border-color:var(--dsw-alias-state-warn-primary,#d9822b);color:var(--dsw-alias-state-warn-primary,#d9822b)}',
|
|
248
|
-
'.dshdl-badge.dshdl-err{border-color:var(--dsw-alias-state-error-primary,#d64545);color:var(--dsw-alias-state-error-primary,#d64545)}',
|
|
249
|
-
'.dshdl-row{display:grid;grid-template-columns:minmax(140px,220px) 1fr;gap:10px;align-items:center}',
|
|
250
|
-
'.dshdl-label{font-size:12.5px;color:var(--dsw-alias-label-secondary,#5b616b)}',
|
|
251
|
-
'.dshdl-hint{font-size:11.5px;color:var(--dsw-alias-label-tertiary,#8a8f98);grid-column:2}',
|
|
252
|
-
'.dshdl-input,.dshdl-select,.dshdl-area{width:100%;box-sizing:border-box;font:inherit;font-size:12.5px;padding:5px 8px;border-radius:7px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.16));background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-primary,inherit)}',
|
|
253
|
-
'.dshdl-area{min-height:62px;resize:vertical;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}',
|
|
254
|
-
'.dshdl-check{width:15px;height:15px}',
|
|
255
|
-
'.dshdl-actions{display:flex;gap:8px;flex-wrap:wrap}',
|
|
256
|
-
'.dshdl-btn{font:inherit;font-size:12.5px;padding:5px 11px;border-radius:7px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.16));background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-primary,inherit);cursor:pointer}',
|
|
257
|
-
'.dshdl-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06))}',
|
|
258
|
-
'.dshdl-btn:disabled{opacity:.5;cursor:default}',
|
|
259
|
-
'.dshdl-btn.dshdl-small{font-size:11.5px;padding:3px 8px}',
|
|
260
|
-
'.dshdl-nodes{max-height:280px;overflow:auto;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:8px}',
|
|
261
|
-
'.dshdl-node{display:flex;align-items:center;gap:8px;padding:5px 9px;font-size:12px;border-bottom:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.06))}',
|
|
262
|
-
'.dshdl-node:last-child{border-bottom:none}',
|
|
263
|
-
'.dshdl-node.dshdl-active{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05))}',
|
|
264
|
-
'.dshdl-nodeName{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
|
265
|
-
'.dshdl-nodeMeta{color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:11.5px;white-space:nowrap}',
|
|
266
|
-
'.dshdl-error{font-size:12px;color:var(--dsw-alias-state-error-primary,#d64545);white-space:pre-wrap;word-break:break-word}',
|
|
267
|
-
'.dshdl-warn{font-size:12px;color:var(--dsw-alias-state-warn-primary,#d9822b)}',
|
|
268
|
-
'.dshdl-empty{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a8f98);padding:8px 2px}',
|
|
269
|
-
'.dshdl-traf{display:flex;flex-direction:column;gap:1px;padding:2px 4px;font-variant-numeric:tabular-nums;line-height:1.25}',
|
|
270
|
-
'.dshdl-trafRow{display:flex;align-items:baseline;gap:4px;font-size:11px;white-space:nowrap}',
|
|
271
|
-
'.dshdl-trafArrow{font-size:10px;width:9px;text-align:center;color:var(--dsw-alias-label-tertiary,#8a8f98)}',
|
|
272
|
-
'.dshdl-trafArrow.up{color:var(--dsw-alias-state-warning-primary,#e0a33e)}',
|
|
273
|
-
'.dshdl-trafArrow.down{color:var(--dsw-alias-state-business-primary,#4c7ef3)}',
|
|
274
|
-
'.dshdl-trafRate{font-weight:600;color:var(--dsw-alias-label-primary,inherit)}',
|
|
275
|
-
'.dshdl-trafSum{margin-left:auto;font-size:10px;color:var(--dsw-alias-label-tertiary,#8a8f98)}',
|
|
276
|
-
'.dshdl-trafRail{display:flex;flex-direction:column;align-items:center;gap:0;font-variant-numeric:tabular-nums;line-height:1.1}',
|
|
277
|
-
'.dshdl-trafRailArrow{font-size:9px;color:var(--dsw-alias-state-business-primary,#4c7ef3)}',
|
|
278
|
-
'.dshdl-trafRailRate{font-size:9px;font-weight:600;color:var(--dsw-alias-label-secondary,#8a8f98)}',
|
|
279
|
-
'.dshdl-importRow{display:flex;flex-direction:column;gap:6px}',
|
|
280
|
-
'.dshdl-fileBtn{display:inline-flex;align-items:center}'
|
|
281
|
-
].join('')
|
|
526
|
+
function h() { return React.createElement.apply(null, arguments) }
|
|
282
527
|
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
528
|
+
function Pill(props) {
|
|
529
|
+
var tone = props.tone === 'ok' ? ' ok' : props.tone === 'err' ? ' err' : props.tone === 'warn' ? ' warn' : ''
|
|
530
|
+
return h('span', { className: 'dshdl-pill' + tone, title: props.title || '' },
|
|
531
|
+
props.tone ? h('span', { className: 'dshdl-dot' }) : null,
|
|
532
|
+
props.children)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function Btn(props) {
|
|
536
|
+
return h('button', {
|
|
537
|
+
type: 'button',
|
|
538
|
+
className: cls('dshdl-btn', props.variant === 'primary' ? 'primary' : props.variant === 'ghost' ? 'ghost' : props.variant === 'danger' ? 'danger' : ''),
|
|
539
|
+
disabled: props.disabled === true,
|
|
540
|
+
onClick: props.onClick,
|
|
541
|
+
title: props.title || ''
|
|
542
|
+
}, props.children)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function Switch(props) {
|
|
546
|
+
return h('button', {
|
|
547
|
+
type: 'button',
|
|
548
|
+
className: cls('dshdl-switch', props.value ? 'on' : ''),
|
|
549
|
+
role: 'switch',
|
|
550
|
+
'aria-checked': props.value ? 'true' : 'false',
|
|
551
|
+
'aria-label': props.label || '',
|
|
552
|
+
disabled: props.disabled === true,
|
|
553
|
+
onClick: function () { if (props.onChange) props.onChange(!props.value) }
|
|
554
|
+
}, h('span', null))
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function Card(props) {
|
|
558
|
+
return h('section', { className: 'dshdl-card' },
|
|
559
|
+
h('h3', { className: 'dshdl-cardTitle' },
|
|
560
|
+
props.title,
|
|
561
|
+
props.titleExtra || null),
|
|
562
|
+
props.hint ? h('p', { className: 'dshdl-cardHint' }, props.hint) : null,
|
|
563
|
+
props.children)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** 一行设置项:左标签+说明,右控件。 */
|
|
567
|
+
function Row(props) {
|
|
568
|
+
return h('div', { className: cls('dshdl-row', props.vertical ? 'vertical' : '') },
|
|
569
|
+
h('div', { className: 'dshdl-rowText' },
|
|
570
|
+
h('div', { className: 'dshdl-rowLabel' }, props.label),
|
|
571
|
+
props.hint ? h('div', { className: 'dshdl-rowHint' }, props.hint) : null),
|
|
572
|
+
h('div', { className: 'dshdl-rowCtl' }, props.children))
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** 文本输入:本地草稿,失焦/回车才提交,避免每敲一个字就写配置。 */
|
|
576
|
+
function TextRow(props) {
|
|
577
|
+
var state = React.useState(props.value === undefined ? '' : String(props.value))
|
|
578
|
+
var draft = state[0]
|
|
579
|
+
var setDraft = state[1]
|
|
580
|
+
React.useEffect(function () { setDraft(props.value === undefined ? '' : String(props.value)) }, [props.value])
|
|
581
|
+
function commit() {
|
|
582
|
+
if (props.onCommit && String(props.value === undefined ? '' : props.value) !== draft) props.onCommit(draft)
|
|
583
|
+
}
|
|
584
|
+
return h(Row, { label: props.label, hint: props.hint },
|
|
585
|
+
h('input', {
|
|
586
|
+
className: 'dshdl-input',
|
|
587
|
+
type: props.type || 'text',
|
|
588
|
+
value: draft,
|
|
589
|
+
placeholder: props.placeholder || '',
|
|
590
|
+
onChange: function (e) { setDraft(e.target.value) },
|
|
591
|
+
onBlur: commit,
|
|
592
|
+
onKeyDown: function (e) { if (e.key === 'Enter') { commit(); e.currentTarget.blur() } }
|
|
593
|
+
}))
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** 多行列表(规则类)用逗号/换行编辑。 */
|
|
597
|
+
function ListRow(props) {
|
|
598
|
+
var value = Array.isArray(props.value) ? props.value.join('\n') : ''
|
|
599
|
+
var state = React.useState(value)
|
|
600
|
+
var draft = state[0]
|
|
601
|
+
var setDraft = state[1]
|
|
602
|
+
React.useEffect(function () { setDraft(Array.isArray(props.value) ? props.value.join('\n') : '') }, [props.value])
|
|
603
|
+
function commit() {
|
|
604
|
+
var next = draft.split(/[\n,]+/).map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 })
|
|
605
|
+
if (props.onCommit) props.onCommit(next)
|
|
606
|
+
}
|
|
607
|
+
return h(Row, { label: props.label, hint: props.hint, vertical: true },
|
|
608
|
+
h('textarea', {
|
|
609
|
+
className: 'dshdl-textarea',
|
|
610
|
+
value: draft,
|
|
611
|
+
placeholder: props.placeholder || '',
|
|
612
|
+
onChange: function (e) { setDraft(e.target.value) },
|
|
613
|
+
onBlur: commit
|
|
614
|
+
}))
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function SelectRow(props) {
|
|
618
|
+
return h(Row, { label: props.label, hint: props.hint },
|
|
619
|
+
h('select', {
|
|
620
|
+
className: 'dshdl-select',
|
|
621
|
+
value: props.value,
|
|
622
|
+
onChange: function (e) { if (props.onCommit) props.onCommit(e.target.value) }
|
|
623
|
+
}, props.options.map(function (option) {
|
|
624
|
+
return h('option', { key: option.value, value: option.value }, option.label)
|
|
625
|
+
})))
|
|
291
626
|
}
|
|
292
627
|
|
|
293
628
|
// ------------------------------------------------------------------
|
|
294
|
-
//
|
|
629
|
+
// 语言:自动(跟随 DSH / 系统)+ 手动覆盖,只作用于本插件页面
|
|
295
630
|
// ------------------------------------------------------------------
|
|
296
|
-
function
|
|
297
|
-
var
|
|
298
|
-
var
|
|
299
|
-
var
|
|
300
|
-
|
|
301
|
-
var
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
type: 'checkbox',
|
|
310
|
-
checked: !!value,
|
|
311
|
-
disabled: disabled,
|
|
312
|
-
onChange: function (e) { onChange(e.target.checked) }
|
|
313
|
-
})
|
|
314
|
-
} else if (type === 'select') {
|
|
315
|
-
control = React.createElement('select', {
|
|
316
|
-
className: 'dshdl-select',
|
|
317
|
-
value: value,
|
|
318
|
-
disabled: disabled,
|
|
319
|
-
onChange: function (e) { onChange(e.target.value); if (onCommit) onCommit(e.target.value) }
|
|
320
|
-
}, (props.options || []).map(function (option) {
|
|
321
|
-
return React.createElement('option', { key: option.value, value: option.value }, option.label)
|
|
322
|
-
}))
|
|
323
|
-
} else if (type === 'textarea') {
|
|
324
|
-
control = React.createElement('textarea', {
|
|
325
|
-
className: 'dshdl-area',
|
|
326
|
-
value: value,
|
|
327
|
-
disabled: disabled,
|
|
328
|
-
spellCheck: false,
|
|
329
|
-
onChange: function (e) { onChange(e.target.value) },
|
|
330
|
-
onBlur: function () { if (onCommit) onCommit(value) }
|
|
331
|
-
})
|
|
332
|
-
} else {
|
|
333
|
-
control = React.createElement('input', {
|
|
334
|
-
className: 'dshdl-input',
|
|
335
|
-
type: type === 'number' ? 'number' : 'text',
|
|
336
|
-
value: value,
|
|
337
|
-
disabled: disabled,
|
|
338
|
-
spellCheck: false,
|
|
339
|
-
onChange: function (e) { onChange(e.target.value) },
|
|
340
|
-
onBlur: function () { if (onCommit) onCommit(value) },
|
|
341
|
-
onKeyDown: function (e) { if (e.key === 'Enter' && onCommit) onCommit(value) }
|
|
631
|
+
function useLocaleSelection(ctxRef) {
|
|
632
|
+
var mode = React.useState(function () { return readStore(LS_LANG, 'auto') })
|
|
633
|
+
var choice = mode[0]
|
|
634
|
+
var setChoice = mode[1]
|
|
635
|
+
// DSH 的语言快照变化时重渲染(auto 模式据此跟随系统)
|
|
636
|
+
var revision = React.useSyncExternalStore(
|
|
637
|
+
function (listener) {
|
|
638
|
+
var service = localeService(ctxRef.current)
|
|
639
|
+
try { return service.subscribe(listener) } catch (e) { return function () {} }
|
|
640
|
+
},
|
|
641
|
+
function () {
|
|
642
|
+
var service = localeService(ctxRef.current)
|
|
643
|
+
try { return service.getSnapshot().revision } catch (e) { return 0 }
|
|
342
644
|
})
|
|
645
|
+
var active = 'en'
|
|
646
|
+
var service = localeService(ctxRef.current)
|
|
647
|
+
try {
|
|
648
|
+
var snap = service.getLocale()
|
|
649
|
+
active = snap && snap.active ? String(snap.active) : 'en'
|
|
650
|
+
} catch (e) { active = 'en' }
|
|
651
|
+
|
|
652
|
+
var resolved = choice === 'auto' ? (/^zh/i.test(active) ? 'zh' : 'en') : choice
|
|
653
|
+
var dict = resolved === 'zh' ? LOCALE_ZH : LOCALE_EN
|
|
654
|
+
var t = React.useCallback(function (key, arg) {
|
|
655
|
+
var text = dict[key]
|
|
656
|
+
if (text === undefined) text = LOCALE_EN[key]
|
|
657
|
+
if (text === undefined) text = key
|
|
658
|
+
return arg === undefined ? text : String(text).replace('%s', String(arg))
|
|
659
|
+
}, [resolved, revision])
|
|
660
|
+
|
|
661
|
+
function pick(next) {
|
|
662
|
+
setChoice(next)
|
|
663
|
+
writeStore(LS_LANG, next)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return { t: t, choice: choice, pick: pick, resolved: resolved }
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function LocaleSwitch(props) {
|
|
670
|
+
var t = props.t
|
|
671
|
+
function item(value, label) {
|
|
672
|
+
return h('button', {
|
|
673
|
+
key: value,
|
|
674
|
+
type: 'button',
|
|
675
|
+
className: props.choice === value ? 'on' : '',
|
|
676
|
+
onClick: function () { props.pick(value) }
|
|
677
|
+
}, label)
|
|
678
|
+
}
|
|
679
|
+
return h('div', { className: 'dshdl-seg', title: props.title || '' },
|
|
680
|
+
item('auto', t('langAuto')),
|
|
681
|
+
item('zh', t('langZh')),
|
|
682
|
+
item('en', t('langEn')))
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ------------------------------------------------------------------
|
|
686
|
+
// 状态轮询
|
|
687
|
+
// ------------------------------------------------------------------
|
|
688
|
+
function useStatus(pollMs) {
|
|
689
|
+
var tuple = React.useState(null)
|
|
690
|
+
var status = tuple[0]
|
|
691
|
+
var setStatus = tuple[1]
|
|
692
|
+
var errTuple = React.useState(null)
|
|
693
|
+
var error = errTuple[0]
|
|
694
|
+
var setError = errTuple[1]
|
|
695
|
+
var alive = React.useRef(true)
|
|
696
|
+
|
|
697
|
+
var reload = React.useCallback(function () {
|
|
698
|
+
return apiFetch('status')
|
|
699
|
+
.then(function (payload) { if (alive.current) { setStatus(payload); setError(null) } return payload })
|
|
700
|
+
.catch(function (e) { if (alive.current) setError(String((e && e.message) || e)) })
|
|
701
|
+
}, [])
|
|
702
|
+
|
|
703
|
+
React.useEffect(function () {
|
|
704
|
+
alive.current = true
|
|
705
|
+
reload()
|
|
706
|
+
if (!pollMs) return function () { alive.current = false }
|
|
707
|
+
var timer = setInterval(function () { if (!document.hidden) reload() }, pollMs)
|
|
708
|
+
return function () { alive.current = false; clearInterval(timer) }
|
|
709
|
+
}, [reload, pollMs])
|
|
710
|
+
|
|
711
|
+
return { status: status, error: error, reload: reload }
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function useTraffic(pollMs) {
|
|
715
|
+
var tuple = React.useState(null)
|
|
716
|
+
var traffic = tuple[0]
|
|
717
|
+
var setTraffic = tuple[1]
|
|
718
|
+
var alive = React.useRef(true)
|
|
719
|
+
React.useEffect(function () {
|
|
720
|
+
alive.current = true
|
|
721
|
+
function tick() {
|
|
722
|
+
apiFetch('traffic')
|
|
723
|
+
.then(function (payload) { if (alive.current) setTraffic(payload) })
|
|
724
|
+
.catch(function () { if (alive.current) setTraffic(null) })
|
|
725
|
+
}
|
|
726
|
+
tick()
|
|
727
|
+
var timer = setInterval(function () { if (!document.hidden) tick() }, pollMs)
|
|
728
|
+
return function () { alive.current = false; clearInterval(timer) }
|
|
729
|
+
}, [pollMs])
|
|
730
|
+
return traffic
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ------------------------------------------------------------------
|
|
734
|
+
// 节点配置卡片(对齐 FlClash 的「配置」页)
|
|
735
|
+
// ------------------------------------------------------------------
|
|
736
|
+
function ProfileTile(props) {
|
|
737
|
+
var profile = props.profile
|
|
738
|
+
var t = props.t
|
|
739
|
+
var openState = React.useState(false)
|
|
740
|
+
var open = openState[0]
|
|
741
|
+
var setOpen = openState[1]
|
|
742
|
+
|
|
743
|
+
var info = profile.subscriptionInfo || null
|
|
744
|
+
var used = null
|
|
745
|
+
var total = null
|
|
746
|
+
if (info) {
|
|
747
|
+
var upSum = Number(info.upload)
|
|
748
|
+
var downSum = Number(info.download)
|
|
749
|
+
var totalSum = Number(info.total)
|
|
750
|
+
if (isFinite(upSum) || isFinite(downSum)) used = (isFinite(upSum) ? upSum : 0) + (isFinite(downSum) ? downSum : 0)
|
|
751
|
+
if (isFinite(totalSum) && totalSum > 0) total = totalSum
|
|
752
|
+
}
|
|
753
|
+
var percent = null
|
|
754
|
+
if (used !== null && total !== null) {
|
|
755
|
+
percent = Math.max(0, Math.min(100, Math.round((used / total) * 100)))
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function act(name) {
|
|
759
|
+
setOpen(false)
|
|
760
|
+
props.onAction(name, profile)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return h('div', {
|
|
764
|
+
className: cls('dshdl-tile', profile.current ? 'on' : ''),
|
|
765
|
+
onClick: function () { if (!open) act('select') },
|
|
766
|
+
title: profile.label
|
|
767
|
+
},
|
|
768
|
+
h('button', {
|
|
769
|
+
className: 'dshdl-more',
|
|
770
|
+
type: 'button',
|
|
771
|
+
'aria-label': 'menu',
|
|
772
|
+
onClick: function (e) { e.stopPropagation(); setOpen(!open) }
|
|
773
|
+
}, '⋯'),
|
|
774
|
+
h('div', { className: 'dshdl-tileName' }, profile.label),
|
|
775
|
+
percent === null
|
|
776
|
+
? h('div', { className: 'dshdl-bar' }, h('span', { style: { width: profile.current ? '100%' : '0%' } }))
|
|
777
|
+
: h('div', { className: 'dshdl-bar' }, h('span', { style: { width: percent + '%' } })),
|
|
778
|
+
h('div', { className: 'dshdl-tileMeta' },
|
|
779
|
+
percent === null
|
|
780
|
+
? (profile.source === 'file' ? t('fileSource') : (profile.autoUpdate ? t('autoUpdateOn', profile.autoUpdateHours || 24) : t('upstream')))
|
|
781
|
+
: (formatBytes(used) + ' / ' + formatBytes(total))),
|
|
782
|
+
h('div', { className: 'dshdl-tileFoot' },
|
|
783
|
+
h('span', null, profile.current ? t('current') : (profile.source === 'file' ? t('fileSource') : '')),
|
|
784
|
+
h('span', null, profile.lastUpdateAt ? (t('updatedAt') + ' ' + formatTime(profile.lastUpdateAt)) : t('never'))),
|
|
785
|
+
open ? h('div', { className: 'dshdl-menu', onClick: function (e) { e.stopPropagation() } },
|
|
786
|
+
h('button', { type: 'button', onClick: function () { act('select') } }, t('use')),
|
|
787
|
+
profile.source === 'url' ? h('button', { type: 'button', onClick: function () { act('update') } }, t('update')) : null,
|
|
788
|
+
h('button', { type: 'button', onClick: function () { act('auto') } }, t('autoUpdate')),
|
|
789
|
+
h('button', { className: 'danger', type: 'button', onClick: function () { act('remove') } }, t('remove'))
|
|
790
|
+
) : null)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ------------------------------------------------------------------
|
|
794
|
+
// 节点卡片(对齐 FlClash 的「代理」页)
|
|
795
|
+
// ------------------------------------------------------------------
|
|
796
|
+
function NodeTile(props) {
|
|
797
|
+
var node = props.node
|
|
798
|
+
var t = props.t
|
|
799
|
+
var delay = node.delay
|
|
800
|
+
var latClass = 'dshdl-lat'
|
|
801
|
+
var latText = t('untested')
|
|
802
|
+
if (delay !== null && delay !== undefined) {
|
|
803
|
+
latClass = delay > 0 ? 'dshdl-lat ok' : 'dshdl-lat bad'
|
|
804
|
+
latText = delay > 0 ? t('ms', delay) : t('timeout')
|
|
343
805
|
}
|
|
344
|
-
return
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
806
|
+
return h('div', {
|
|
807
|
+
className: cls('dshdl-tile', props.active ? 'on' : '', delay === 0 ? 'dead' : ''),
|
|
808
|
+
onClick: props.onClick,
|
|
809
|
+
title: (node.server || node.name) + (node.needsNative ? ' · ' + t('needsNative') : '')
|
|
810
|
+
},
|
|
811
|
+
h('div', { className: 'dshdl-tileName' }, node.name),
|
|
812
|
+
h('div', { className: 'dshdl-tileMeta' }, node.type),
|
|
813
|
+
h('div', { className: 'dshdl-tileFoot' }, h('span', { className: latClass }, latText)))
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function AutoTile(props) {
|
|
817
|
+
return h('div', {
|
|
818
|
+
className: cls('dshdl-tile', props.active ? 'on' : ''),
|
|
819
|
+
onClick: props.onClick
|
|
820
|
+
},
|
|
821
|
+
h('div', { className: 'dshdl-tileName' }, props.t('nodeAuto')),
|
|
822
|
+
h('div', { className: 'dshdl-tileMeta' }, props.t('upstream')),
|
|
823
|
+
h('div', { className: 'dshdl-tileFoot' }, h('span', { className: 'dshdl-lat' }, props.current || '')))
|
|
348
824
|
}
|
|
349
825
|
|
|
350
|
-
|
|
826
|
+
// ------------------------------------------------------------------
|
|
827
|
+
// 主页面:两个入口共用
|
|
828
|
+
// ------------------------------------------------------------------
|
|
829
|
+
function DownloaderPage(props) {
|
|
830
|
+
var ctxRef = props.ctxRef
|
|
831
|
+
var variant = props.variant || 'panel'
|
|
832
|
+
var compact = variant === 'settings'
|
|
351
833
|
var scope = props.scope
|
|
352
|
-
var t = props.t || function (key) { return key }
|
|
353
834
|
|
|
354
|
-
|
|
835
|
+
var locale = useLocaleSelection(ctxRef)
|
|
836
|
+
var t = locale.t
|
|
837
|
+
|
|
838
|
+
var state = useStatus(compact ? 6000 : 5000)
|
|
839
|
+
var status = state.status
|
|
840
|
+
var error = state.error
|
|
841
|
+
var reload = state.reload
|
|
842
|
+
var traffic = useTraffic(2000)
|
|
843
|
+
|
|
844
|
+
var advancedState = React.useState(function () { return readStore(LS_ADVANCED, '0') === '1' })
|
|
845
|
+
var advanced = advancedState[0]
|
|
846
|
+
var setAdvanced = advancedState[1]
|
|
847
|
+
|
|
848
|
+
var nodesState = React.useState([])
|
|
849
|
+
var nodes = nodesState[0]
|
|
850
|
+
var setNodes = nodesState[1]
|
|
851
|
+
var groupsState = React.useState([])
|
|
852
|
+
var groups = groupsState[0]
|
|
853
|
+
var setGroups = groupsState[1]
|
|
854
|
+
var guardState = React.useState(null)
|
|
855
|
+
var guard = guardState[0]
|
|
856
|
+
var setGuard = guardState[1]
|
|
857
|
+
|
|
858
|
+
var busyState = React.useState(null)
|
|
859
|
+
var busy = busyState[0]
|
|
860
|
+
var setBusy = busyState[1]
|
|
861
|
+
var actionErrorState = React.useState(null)
|
|
862
|
+
var actionError = actionErrorState[0]
|
|
863
|
+
var setActionError = actionErrorState[1]
|
|
864
|
+
var noticeState = React.useState(null)
|
|
865
|
+
var notice = noticeState[0]
|
|
866
|
+
var setNotice = noticeState[1]
|
|
867
|
+
|
|
868
|
+
// 配置表单
|
|
355
869
|
var snap = scope
|
|
356
870
|
? React.useSyncExternalStore(
|
|
357
871
|
function (listener) { return scope.subscribe(listener) },
|
|
@@ -359,445 +873,590 @@ window.__ModuleLoader__.load({
|
|
|
359
873
|
: { status: 'unavailable', value: undefined }
|
|
360
874
|
|
|
361
875
|
var stored = Object.assign({}, DEFAULTS, (snap && snap.value) || {})
|
|
362
|
-
var
|
|
363
|
-
var
|
|
364
|
-
var
|
|
365
|
-
var [groups, setGroups] = React.useState([])
|
|
366
|
-
var [guard, setGuard] = React.useState(null)
|
|
367
|
-
var [importUrl, setImportUrl] = React.useState('')
|
|
368
|
-
var [importLabel, setImportLabel] = React.useState('')
|
|
369
|
-
var [busy, setBusy] = React.useState(null)
|
|
370
|
-
var [error, setError] = React.useState(null)
|
|
371
|
-
|
|
876
|
+
var draftState = React.useState({})
|
|
877
|
+
var draft = draftState[0]
|
|
878
|
+
var setDraft = draftState[1]
|
|
372
879
|
var value = Object.assign({}, stored, draft)
|
|
880
|
+
var persistable = !!scope
|
|
373
881
|
|
|
374
|
-
function
|
|
882
|
+
function setField(field, next) {
|
|
375
883
|
setDraft(function (prev) { var copy = Object.assign({}, prev); copy[field] = next; return copy })
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function commit(field, next) {
|
|
379
884
|
if (!scope) return
|
|
380
|
-
setDraft(function (prev) { var copy = Object.assign({}, prev); delete copy[field]; return copy })
|
|
381
885
|
try {
|
|
382
886
|
var promise = scope.set(field, next)
|
|
383
887
|
if (promise && typeof promise.catch === 'function') {
|
|
384
|
-
promise.catch(function (e) {
|
|
888
|
+
promise.catch(function (e) {
|
|
889
|
+
setActionError(String((e && e.message) || e))
|
|
890
|
+
setDraft(function (prev) { var copy = Object.assign({}, prev); delete copy[field]; return copy })
|
|
891
|
+
})
|
|
385
892
|
}
|
|
386
|
-
} catch (e) {
|
|
387
|
-
setError(String((e && e.message) || e))
|
|
388
|
-
}
|
|
893
|
+
} catch (e) { setActionError(String((e && e.message) || e)) }
|
|
389
894
|
}
|
|
390
895
|
|
|
391
|
-
function
|
|
392
|
-
|
|
393
|
-
|
|
896
|
+
var loadProxies = React.useCallback(function () {
|
|
897
|
+
return apiFetch('proxies').then(function (payload) {
|
|
898
|
+
setNodes(Array.isArray(payload.nodes) ? payload.nodes : [])
|
|
899
|
+
setGroups(Array.isArray(payload.groups) ? payload.groups : [])
|
|
900
|
+
return payload
|
|
901
|
+
}).catch(function (e) { setActionError(String((e && e.message) || e)) })
|
|
902
|
+
}, [])
|
|
903
|
+
|
|
904
|
+
var loadGuard = React.useCallback(function () {
|
|
905
|
+
return apiFetch('guard').then(function (payload) { setGuard(payload) })
|
|
906
|
+
.catch(function () { setGuard(null) })
|
|
907
|
+
}, [])
|
|
908
|
+
|
|
909
|
+
React.useEffect(function () { loadProxies() }, [loadProxies])
|
|
910
|
+
React.useEffect(function () { loadGuard() }, [loadGuard])
|
|
911
|
+
|
|
912
|
+
function run(key, fn) {
|
|
913
|
+
if (busy) return
|
|
914
|
+
setBusy(key)
|
|
915
|
+
setActionError(null)
|
|
916
|
+
setNotice(null)
|
|
917
|
+
Promise.resolve()
|
|
918
|
+
.then(fn)
|
|
919
|
+
.then(function (result) {
|
|
920
|
+
setBusy(null)
|
|
921
|
+
return reload().then(function () { return loadProxies() }).then(function () {
|
|
922
|
+
if (result && result.hint) setNotice(result.hint)
|
|
923
|
+
})
|
|
924
|
+
})
|
|
925
|
+
.catch(function (e) {
|
|
926
|
+
setBusy(null)
|
|
927
|
+
setActionError(String((e && e.message) || e))
|
|
928
|
+
})
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function refreshAll() {
|
|
932
|
+
run('refresh', function () { return apiFetch('status?refresh=1') })
|
|
394
933
|
}
|
|
395
934
|
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
if (!isFinite(parsed)) { commit(field, stored[field]); return }
|
|
399
|
-
commit(field, parsed)
|
|
935
|
+
function restartCore() {
|
|
936
|
+
run('restart', function () { return apiFetch('restart', {}) })
|
|
400
937
|
}
|
|
401
938
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return
|
|
409
|
-
var
|
|
410
|
-
|
|
411
|
-
|
|
939
|
+
function updateSubscription() {
|
|
940
|
+
run('update', function () { return apiFetch('update-subscription', {}) })
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function testAll() {
|
|
944
|
+
run('test', function () {
|
|
945
|
+
return apiFetch('delay-all', {}).then(function (result) {
|
|
946
|
+
var map = {}
|
|
947
|
+
var results = (result && result.results) || {}
|
|
948
|
+
Object.keys(results).forEach(function (key) { map[key] = results[key] })
|
|
949
|
+
setNodes(function (prev) {
|
|
950
|
+
return prev.map(function (node) {
|
|
951
|
+
if (map[node.name] === undefined) return node
|
|
952
|
+
var delay = map[node.name]
|
|
953
|
+
return Object.assign({}, node, { delay: delay, alive: delay !== null && delay > 0 })
|
|
954
|
+
})
|
|
955
|
+
})
|
|
956
|
+
return null
|
|
412
957
|
})
|
|
413
958
|
})
|
|
414
|
-
}
|
|
959
|
+
}
|
|
415
960
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
.catch(function (e) { setError(String((e && e.message) || e)) })
|
|
420
|
-
}, [api])
|
|
961
|
+
function selectNode(name) {
|
|
962
|
+
run('select:' + name, function () { return apiFetch('select', { name: name }) })
|
|
963
|
+
}
|
|
421
964
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
965
|
+
function testNode(name) {
|
|
966
|
+
run('delay:' + name, function () {
|
|
967
|
+
return apiFetch('delay', { name: name }).then(function (result) {
|
|
968
|
+
setNodes(function (prev) {
|
|
969
|
+
return prev.map(function (node) {
|
|
970
|
+
if (node.name !== name) return node
|
|
971
|
+
return Object.assign({}, node, { delay: result.delay, alive: result.delay > 0 })
|
|
972
|
+
})
|
|
973
|
+
})
|
|
974
|
+
return null
|
|
975
|
+
})
|
|
976
|
+
})
|
|
977
|
+
}
|
|
427
978
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
979
|
+
function profileAction(name, profile) {
|
|
980
|
+
if (name === 'select') return selectProfile(profile.id)
|
|
981
|
+
if (name === 'update') return run('p-update', function () { return apiFetch('profile-update', { id: profile.id }) })
|
|
982
|
+
if (name === 'remove') return run('p-remove', function () { return apiFetch('profile-remove', { id: profile.id }) })
|
|
983
|
+
if (name === 'auto') {
|
|
984
|
+
return run('p-auto', function () {
|
|
985
|
+
return apiFetch('profile-auto', { id: profile.id, enabled: !profile.autoUpdate, hours: profile.autoUpdateHours || 24 })
|
|
986
|
+
})
|
|
987
|
+
}
|
|
988
|
+
return null
|
|
989
|
+
}
|
|
433
990
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
var
|
|
440
|
-
var
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
.then(function () { setBusy(null) })
|
|
991
|
+
function selectProfile(id) {
|
|
992
|
+
return run('p-select', function () { return apiFetch('profile-select', { id: id }) })
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function addProfileFromUrl() {
|
|
996
|
+
var urlState = importUrlState[0]
|
|
997
|
+
var url = urlState
|
|
998
|
+
if (!url.trim()) return
|
|
999
|
+
run('p-add', function () {
|
|
1000
|
+
return apiFetch('profile-add-url', { value: url.trim(), label: importLabelState[0].trim() || undefined }).then(function (result) {
|
|
1001
|
+
setImportUrl('')
|
|
1002
|
+
setImportLabel('')
|
|
1003
|
+
setShowImport(false)
|
|
1004
|
+
return result
|
|
1005
|
+
})
|
|
1006
|
+
})
|
|
451
1007
|
}
|
|
452
1008
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
1009
|
+
function addProfileFromFile(file) {
|
|
1010
|
+
if (!file) return
|
|
1011
|
+
var reader = new FileReader()
|
|
1012
|
+
reader.onload = function () {
|
|
1013
|
+
var content = String(reader.result || '')
|
|
1014
|
+
run('p-add-file', function () { return apiFetch('profile-add-file', { content: content, name: file.name }) })
|
|
1015
|
+
}
|
|
1016
|
+
reader.readAsText(file)
|
|
458
1017
|
}
|
|
459
1018
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
onChange: function (v) { set('proxyUrl', v) }, onCommit: function (v) { commit('proxyUrl', v) } }),
|
|
466
|
-
React.createElement(Field, { key: 'subscriptionUrl', label: 'subscriptionUrl', value: value.subscriptionUrl, type: 'text',
|
|
467
|
-
hint: 'Clash 订阅地址。自包含内核从这里取节点,不需要本机装 Clash。',
|
|
468
|
-
onChange: function (v) { set('subscriptionUrl', v) }, onCommit: function (v) { commit('subscriptionUrl', v) } }),
|
|
469
|
-
React.createElement(Field, { key: 'fetchProxyUrl', label: 'fetchProxyUrl', value: value.fetchProxyUrl, type: 'text',
|
|
470
|
-
hint: '仅用于抓订阅本身(订阅被墙时填),必须是 http(s):// 代理。',
|
|
471
|
-
onChange: function (v) { set('fetchProxyUrl', v) }, onCommit: function (v) { commit('fetchProxyUrl', v) } }),
|
|
472
|
-
React.createElement(Field, { key: 'groupType', label: 'groupType', value: value.groupType, type: 'select',
|
|
473
|
-
options: [
|
|
474
|
-
{ value: 'url-test', label: 'url-test(自动最快)' },
|
|
475
|
-
{ value: 'select', label: 'select(手动)' },
|
|
476
|
-
{ value: 'fallback', label: 'fallback(失败切换)' }
|
|
477
|
-
],
|
|
478
|
-
onChange: function (v) { set('groupType', v) }, onCommit: function (v) { commit('groupType', v) } }),
|
|
479
|
-
React.createElement(Field, { key: 'preferredNode', label: 'preferredNode', value: value.preferredNode, type: 'text',
|
|
480
|
-
hint: '按名字固定一个节点,优先级最高。',
|
|
481
|
-
onChange: function (v) { set('preferredNode', v) }, onCommit: function (v) { commit('preferredNode', v) } }),
|
|
482
|
-
React.createElement(Field, { key: 'autoUpdateHours', label: 'autoUpdateHours', value: value.autoUpdateHours, type: 'number',
|
|
483
|
-
hint: '订阅自动更新间隔(小时,0 关闭)。',
|
|
484
|
-
onChange: function (v) { set('autoUpdateHours', v) }, onCommit: function (v) { commitNumber('autoUpdateHours', v) } }),
|
|
485
|
-
React.createElement(Field, { key: 'latencyTestUrl', label: 'latencyTestUrl', value: value.latencyTestUrl, type: 'text',
|
|
486
|
-
onChange: function (v) { set('latencyTestUrl', v) }, onCommit: function (v) { commit('latencyTestUrl', v) } }),
|
|
487
|
-
React.createElement(Field, { key: 'latencyTimeoutMs', label: 'latencyTimeoutMs', value: value.latencyTimeoutMs, type: 'number',
|
|
488
|
-
onChange: function (v) { set('latencyTimeoutMs', v) }, onCommit: function (v) { commitNumber('latencyTimeoutMs', v) } })
|
|
489
|
-
]
|
|
490
|
-
|
|
491
|
-
var rulesRows = [
|
|
492
|
-
React.createElement(Field, { key: 'protectAiPlatforms', label: 'protectAiPlatforms', value: value.protectAiPlatforms, type: 'bool',
|
|
493
|
-
hint: '国内 AI 平台域名(DeepSeek / 智谱 / Kimi / 通义 / 豆包 / 文心 / 混元 / 星火 / MiniMax …)强制直连,优先级高于一切规则,连"全部走代理"模式也不例外。',
|
|
494
|
-
onChange: function (v) { commitBool('protectAiPlatforms', v) } }),
|
|
495
|
-
React.createElement(Field, { key: 'domesticDirect', label: 'domesticDirect', value: value.domesticDirect, type: 'bool',
|
|
496
|
-
hint: '开:境内直连、境外走代理。关:所有下载都走代理(AI 平台仍直连)。',
|
|
497
|
-
onChange: function (v) { commitBool('domesticDirect', v) } }),
|
|
498
|
-
React.createElement(Field, { key: 'extraDirectDomains', label: 'extraDirectDomains', value: arrayToLines(value.extraDirectDomains), type: 'textarea',
|
|
499
|
-
hint: '每行一个域名或完整 URL,加进强制直连名单。',
|
|
500
|
-
onChange: function (v) { set('extraDirectDomains', v) }, onCommit: function (v) { commit('extraDirectDomains', linesToArray(v)) } }),
|
|
501
|
-
React.createElement(Field, { key: 'guardShellDownloads', label: 'guardShellDownloads', value: value.guardShellDownloads, type: 'bool',
|
|
502
|
-
hint: '检测到 git / curl 等命令要从境外下载、又没带代理时拦下来,提示改用 dsh_run_proxied(命令里已带代理、或内核无可用出口时不拦)。',
|
|
503
|
-
onChange: function (v) { commitBool('guardShellDownloads', v) } }),
|
|
504
|
-
React.createElement(Field, { key: 'extraRules', label: 'extraRules', value: arrayToLines(value.extraRules), type: 'textarea',
|
|
505
|
-
hint: '每行一条,如 DOMAIN-SUFFIX,example.com,DIRECT。',
|
|
506
|
-
onChange: function (v) { set('extraRules', v) }, onCommit: function (v) { commit('extraRules', linesToArray(v)) } }),
|
|
507
|
-
React.createElement(Field, { key: 'excludeRules', label: 'excludeRules', value: arrayToLines(value.excludeRules), type: 'textarea',
|
|
508
|
-
hint: '每行一条,从订阅规则里剔除(子串匹配)。',
|
|
509
|
-
onChange: function (v) { set('excludeRules', v) }, onCommit: function (v) { commit('excludeRules', linesToArray(v)) } })
|
|
510
|
-
]
|
|
511
|
-
|
|
512
|
-
var downloadRows = [
|
|
513
|
-
React.createElement(Field, { key: 'downloadDir', label: 'downloadDir', value: value.downloadDir, type: 'text',
|
|
514
|
-
hint: '空 = $DSH_HOME/downloads。',
|
|
515
|
-
onChange: function (v) { set('downloadDir', v) }, onCommit: function (v) { commit('downloadDir', v) } }),
|
|
516
|
-
React.createElement(Field, { key: 'maxDownloadMb', label: 'maxDownloadMb', value: value.maxDownloadMb, type: 'number',
|
|
517
|
-
onChange: function (v) { set('maxDownloadMb', v) }, onCommit: function (v) { commitNumber('maxDownloadMb', v) } }),
|
|
518
|
-
React.createElement(Field, { key: 'downloadTimeoutS', label: 'downloadTimeoutS', value: value.downloadTimeoutS, type: 'number',
|
|
519
|
-
onChange: function (v) { set('downloadTimeoutS', v) }, onCommit: function (v) { commitNumber('downloadTimeoutS', v) } }),
|
|
520
|
-
React.createElement(Field, { key: 'stallTimeoutS', label: 'stallTimeoutS', value: value.stallTimeoutS, type: 'number',
|
|
521
|
-
onChange: function (v) { set('stallTimeoutS', v) }, onCommit: function (v) { commitNumber('stallTimeoutS', v) } }),
|
|
522
|
-
React.createElement(Field, { key: 'threads', label: 'threads', value: value.threads, type: 'number',
|
|
523
|
-
hint: '多线程下载的分片连接数,1 = 单连接。服务端不支持 Range(或返回的 Range 不可信)时自动回退单连接;文件小于 1 MiB 也用单连接。',
|
|
524
|
-
onChange: function (v) { set('threads', v) }, onCommit: function (v) { commitNumber('threads', v) } }),
|
|
525
|
-
React.createElement(Field, { key: 'maxRedirects', label: 'maxRedirects', value: value.maxRedirects, type: 'number',
|
|
526
|
-
onChange: function (v) { set('maxRedirects', v) }, onCommit: function (v) { commitNumber('maxRedirects', v) } }),
|
|
527
|
-
React.createElement(Field, { key: 'insecureTls', label: 'insecureTls', value: value.insecureTls, type: 'bool',
|
|
528
|
-
hint: '跳过 TLS 校验,仅用于企业 TLS 解密环境。',
|
|
529
|
-
onChange: function (v) { commitBool('insecureTls', v) } }),
|
|
530
|
-
React.createElement(Field, { key: 'allowOutsideWorkspace', label: 'allowOutsideWorkspace', value: value.allowOutsideWorkspace, type: 'bool',
|
|
531
|
-
hint: '允许 save_path 写到工作区与下载目录之外。',
|
|
532
|
-
onChange: function (v) { commitBool('allowOutsideWorkspace', v) } })
|
|
533
|
-
]
|
|
534
|
-
|
|
535
|
-
var nodeRows = nodes.length === 0
|
|
536
|
-
? React.createElement('div', { className: 'dshdl-empty' }, '暂无节点。填好 subscriptionUrl 后点「更新订阅」。')
|
|
537
|
-
: nodes.map(function (node) {
|
|
538
|
-
var active = status && status.selected === node.name
|
|
539
|
-
return React.createElement('div', { key: node.name, className: 'dshdl-node' + (active ? ' dshdl-active' : '') },
|
|
540
|
-
React.createElement('span', { className: 'dshdl-nodeName', title: node.name }, node.name),
|
|
541
|
-
React.createElement('span', { className: 'dshdl-nodeMeta' },
|
|
542
|
-
(node.type || '?') + (node.needsNative ? ' · 需原生连接器' : '') + (node.delay === null ? '' : ' · ' + node.delay + 'ms')),
|
|
543
|
-
React.createElement('button', {
|
|
544
|
-
type: 'button', className: 'dshdl-btn dshdl-small', disabled: !!busy,
|
|
545
|
-
onClick: function () {
|
|
546
|
-
run('delay:' + node.name, function () { return api('delay', { name: node.name }).then(loadNodes) })
|
|
547
|
-
}
|
|
548
|
-
}, t('test')),
|
|
549
|
-
React.createElement('button', {
|
|
550
|
-
type: 'button', className: 'dshdl-btn dshdl-small', disabled: !!busy || active,
|
|
551
|
-
onClick: function () {
|
|
552
|
-
run('select:' + node.name, function () { return api('select', { name: node.name }).then(loadStatus).then(loadNodes) })
|
|
553
|
-
}
|
|
554
|
-
}, t('select')))
|
|
1019
|
+
function applyGuard() {
|
|
1020
|
+
run('guard', function () {
|
|
1021
|
+
return apiFetch('guard-apply', {}).then(function (result) {
|
|
1022
|
+
loadGuard()
|
|
1023
|
+
return { hint: t('guardRestart') }
|
|
555
1024
|
})
|
|
1025
|
+
})
|
|
1026
|
+
}
|
|
556
1027
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
1028
|
+
function restoreGuard() {
|
|
1029
|
+
run('guard-restore', function () {
|
|
1030
|
+
return apiFetch('guard-restore', {}).then(function () { loadGuard(); return { hint: t('guardRestart') } })
|
|
1031
|
+
})
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// --- 派生状态 ---
|
|
1035
|
+
var coreState = status ? status.state : 'idle'
|
|
1036
|
+
var tone = 'idle'
|
|
1037
|
+
var stateText = t('stIdle')
|
|
1038
|
+
if (coreState === 'running') { tone = 'ok'; stateText = t('stRunning') }
|
|
1039
|
+
else if (coreState === 'disabled') { tone = 'idle'; stateText = t('stDisabled') }
|
|
1040
|
+
else if (coreState === 'failed') { tone = 'err'; stateText = t('stFailed') }
|
|
1041
|
+
else if (coreState === 'no-subscription') { tone = 'warn'; stateText = t('stNoSubscription') }
|
|
1042
|
+
else if (status && status.enabled === false) { tone = 'idle'; stateText = t('stDisabled') }
|
|
1043
|
+
|
|
1044
|
+
var mode = status ? status.mode : 'none'
|
|
1045
|
+
var modeText = mode === 'subscription' ? t('modeSubscription')
|
|
1046
|
+
: mode === 'explicit' ? t('modeExplicit')
|
|
1047
|
+
: mode === 'environment' ? t('modeEnvironment')
|
|
1048
|
+
: t('modeNone')
|
|
1049
|
+
|
|
1050
|
+
var profiles = (status && Array.isArray(status.profiles)) ? status.profiles : []
|
|
1051
|
+
var selectedNode = (status && status.selected) || null
|
|
1052
|
+
var currentNode = (status && status.currentNode) || null
|
|
1053
|
+
var running = status && status.state === 'running'
|
|
1054
|
+
|
|
1055
|
+
// --- 导入表单状态 ---
|
|
1056
|
+
var importUrlPair = React.useState('')
|
|
1057
|
+
var importUrlState = importUrlPair
|
|
1058
|
+
var setImportUrl = importUrlPair[1]
|
|
1059
|
+
var importLabelPair = React.useState('')
|
|
1060
|
+
var importLabelState = importLabelPair
|
|
1061
|
+
var setImportLabel = importLabelPair[1]
|
|
1062
|
+
var showImportPair = React.useState(false)
|
|
1063
|
+
var showImport = showImportPair[0]
|
|
1064
|
+
var setShowImport = showImportPair[1]
|
|
1065
|
+
|
|
1066
|
+
var header = h('header', { className: 'dshdl-head' },
|
|
1067
|
+
h('div', { className: 'dshdl-headText' },
|
|
1068
|
+
h('h2', { className: 'dshdl-h1' }, t('title')),
|
|
1069
|
+
h('p', { className: 'dshdl-sub' }, t('subtitle'))),
|
|
1070
|
+
h('div', { className: 'dshdl-actions' },
|
|
1071
|
+
h(Pill, { tone: tone }, stateText),
|
|
1072
|
+
h(LocaleSwitch, { t: t, choice: locale.choice, pick: locale.pick }),
|
|
1073
|
+
h(Btn, { onClick: refreshAll, disabled: busy === 'refresh' }, busy === 'refresh' ? '…' : t('refresh')),
|
|
1074
|
+
h(Btn, { onClick: restartCore, disabled: busy === 'restart' }, t('restartCore'))))
|
|
1075
|
+
|
|
1076
|
+
var errorNode = error ? h('div', { className: 'dshdl-err' }, error) : null
|
|
1077
|
+
var actionErrorNode = actionError ? h('div', { className: 'dshdl-err' }, actionError) : null
|
|
1078
|
+
var noticeNode = notice ? h('div', { className: 'dshdl-note' }, notice) : null
|
|
1079
|
+
|
|
1080
|
+
if (!persistable) {
|
|
1081
|
+
errorNode = h('div', { className: 'dshdl-err' }, t('unavailable'))
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// --- 状态卡 ---
|
|
1085
|
+
var statusCard = h(Card, { title: t('upstream') },
|
|
1086
|
+
h('dl', { className: 'dshdl-kv' },
|
|
1087
|
+
h('dt', null, t('upstream')),
|
|
1088
|
+
h('dd', null, modeText + (status && status.reason ? ' · ' + status.reason : '')),
|
|
1089
|
+
h('dt', null, t('corePort')),
|
|
1090
|
+
h('dd', null, status && status.port ? String(status.port) : t('coreNone')),
|
|
1091
|
+
h('dt', null, t('nodesCount')),
|
|
1092
|
+
h('dd', null, status ? String(status.nodeCount) + ' · ' + (status.unsupportedNodes ? String(status.unsupportedNodes) : '0') + ' ⚠' : '—'),
|
|
1093
|
+
h('dt', null, t('currentNode')),
|
|
1094
|
+
h('dd', null, currentNode || '—')),
|
|
1095
|
+
!running && status ? h('div', { className: 'dshdl-note', style: { marginTop: '12px' } }, t('notRunningHint')) : null)
|
|
1096
|
+
|
|
1097
|
+
// --- 节点配置卡 ---
|
|
1098
|
+
var profilesCard = h(Card, { title: t('profiles'), hint: t('profilesHint') },
|
|
1099
|
+
profiles.length === 0
|
|
1100
|
+
? h('div', { className: 'dshdl-empty' }, t('emptyProfiles'))
|
|
1101
|
+
: h('div', { className: 'dshdl-grid' }, profiles.map(function (profile) {
|
|
1102
|
+
return h(ProfileTile, { key: profile.id, profile: profile, t: t, onAction: profileAction })
|
|
1103
|
+
})),
|
|
1104
|
+
showImport
|
|
1105
|
+
? h('div', { className: 'dshdl-inlineForm' },
|
|
1106
|
+
h('div', { className: 'dshdl-inlineRow' },
|
|
1107
|
+
h('input', {
|
|
1108
|
+
className: 'dshdl-input',
|
|
636
1109
|
placeholder: t('importUrlPlaceholder'),
|
|
1110
|
+
value: importUrlState[0],
|
|
637
1111
|
onChange: function (e) { setImportUrl(e.target.value) }
|
|
638
1112
|
}),
|
|
639
|
-
|
|
640
|
-
className: 'dshdl-input',
|
|
641
|
-
placeholder: t('
|
|
1113
|
+
h('input', {
|
|
1114
|
+
className: 'dshdl-input',
|
|
1115
|
+
placeholder: t('importLabel'),
|
|
1116
|
+
value: importLabelState[0],
|
|
642
1117
|
onChange: function (e) { setImportLabel(e.target.value) }
|
|
643
|
-
}),
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
reader.readAsText(file)
|
|
673
|
-
}
|
|
674
|
-
})))))),
|
|
675
|
-
|
|
676
|
-
React.createElement('div', { className: 'dshdl-card' },
|
|
677
|
-
React.createElement('div', { className: 'dshdl-cardHead' },
|
|
678
|
-
React.createElement('span', { className: 'dshdl-cardTitle' }, t('sessionGuard')),
|
|
679
|
-
React.createElement('div', { className: 'dshdl-badges' },
|
|
680
|
-
React.createElement('span', {
|
|
681
|
-
className: guard && guard.covered ? 'dshdl-badge dshdl-ok' : 'dshdl-badge dshdl-warn'
|
|
682
|
-
}, guard
|
|
683
|
-
? (guard.covered ? t('guardCovered') : t('guardMissing') + ' ' + ((guard.missing && guard.missing.length) || 0))
|
|
684
|
-
: '…'))),
|
|
685
|
-
React.createElement('p', { className: 'dshdl-desc' }, t('guardIntro')),
|
|
686
|
-
guard ? React.createElement('p', { className: 'dshdl-desc' }, guard.env_path) : null,
|
|
687
|
-
guard && Array.isArray(guard.proxy_vars) && guard.proxy_vars.length === 0
|
|
688
|
-
? React.createElement('p', { className: 'dshdl-warn' }, t('guardNoProxyVars'))
|
|
689
|
-
: null,
|
|
690
|
-
React.createElement('div', { className: 'dshdl-actions' },
|
|
691
|
-
React.createElement('button', {
|
|
692
|
-
type: 'button', className: 'dshdl-btn', disabled: !!busy || !!(guard && guard.covered),
|
|
693
|
-
onClick: function () {
|
|
694
|
-
run('guard-apply', function () {
|
|
695
|
-
return api('guard-apply', {}).then(function (payload) { setGuard(payload.state); return loadStatus(false) })
|
|
1118
|
+
})),
|
|
1119
|
+
h('div', { className: 'dshdl-inlineRow' },
|
|
1120
|
+
h(Btn, { variant: 'primary', onClick: addProfileFromUrl, disabled: busy === 'p-add' }, t('import')),
|
|
1121
|
+
h('label', { className: 'dshdl-btn' }, t('importFile'),
|
|
1122
|
+
h('input', {
|
|
1123
|
+
type: 'file',
|
|
1124
|
+
accept: '.yaml,.yml,.txt,.conf',
|
|
1125
|
+
style: { display: 'none' },
|
|
1126
|
+
onChange: function (e) { addProfileFromFile(e.target.files && e.target.files[0]) }
|
|
1127
|
+
})),
|
|
1128
|
+
h(Btn, { variant: 'ghost', onClick: function () { setShowImport(false) } }, t('cancel'))))
|
|
1129
|
+
: h('div', { style: { display: 'flex', justifyContent: 'flex-end', marginTop: '14px' } },
|
|
1130
|
+
h(Btn, { variant: 'primary', onClick: function () { setShowImport(true) } }, '+ ' + t('addProfile'))))
|
|
1131
|
+
|
|
1132
|
+
// --- 节点卡 ---
|
|
1133
|
+
var nodesCard = h(Card, { title: t('nodes'), hint: t('nodesHint') },
|
|
1134
|
+
h('div', { className: 'dshdl-grid' },
|
|
1135
|
+
h(AutoTile, {
|
|
1136
|
+
t: t,
|
|
1137
|
+
active: selectedNode === null || selectedNode === undefined,
|
|
1138
|
+
current: currentNode || '',
|
|
1139
|
+
onClick: function () {
|
|
1140
|
+
// 宿主可能是还没重启的旧版本,没有 select-auto 路由 —— 退回「选中 AUTO 组的当前节点」。
|
|
1141
|
+
run('select-auto', function () {
|
|
1142
|
+
return apiFetch('select-auto', {}).catch(function () {
|
|
1143
|
+
var auto = groups.filter(function (group) { return group.name === 'AUTO' })[0]
|
|
1144
|
+
var target = auto && auto.now ? auto.now : (nodes[0] && nodes[0].name)
|
|
1145
|
+
if (!target) return null
|
|
1146
|
+
return apiFetch('select', { name: target })
|
|
696
1147
|
})
|
|
1148
|
+
})
|
|
1149
|
+
}
|
|
1150
|
+
}),
|
|
1151
|
+
nodes.map(function (node) {
|
|
1152
|
+
return h(NodeTile, {
|
|
1153
|
+
key: node.name,
|
|
1154
|
+
node: node,
|
|
1155
|
+
t: t,
|
|
1156
|
+
active: selectedNode === node.name,
|
|
1157
|
+
onClick: function () { selectNode(node.name) }
|
|
1158
|
+
})
|
|
1159
|
+
})),
|
|
1160
|
+
h('div', { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '14px' } },
|
|
1161
|
+
h(Btn, { onClick: testAll, disabled: busy === 'test' }, busy === 'test' ? t('testing') : t('testAll')),
|
|
1162
|
+
h(Btn, { onClick: updateSubscription, disabled: busy === 'update' }, t('update'))))
|
|
1163
|
+
|
|
1164
|
+
// --- 下载卡(基础) ---
|
|
1165
|
+
var downloadCard = h(Card, { title: t('download') },
|
|
1166
|
+
h(TextRow, {
|
|
1167
|
+
label: t('downloadDir'),
|
|
1168
|
+
hint: t('downloadDirHint'),
|
|
1169
|
+
placeholder: t('dirPlaceholder'),
|
|
1170
|
+
value: value.downloadDir,
|
|
1171
|
+
onCommit: function (next) { setField('downloadDir', next) }
|
|
1172
|
+
}),
|
|
1173
|
+
h(Row, { label: t('threads'), hint: t('threadsHint') },
|
|
1174
|
+
h('input', {
|
|
1175
|
+
className: 'dshdl-input',
|
|
1176
|
+
type: 'number',
|
|
1177
|
+
min: 1,
|
|
1178
|
+
max: 32,
|
|
1179
|
+
value: value.threads,
|
|
1180
|
+
style: { width: '90px', minWidth: '90px' },
|
|
1181
|
+
onChange: function (e) {
|
|
1182
|
+
var parsed = Number(e.target.value)
|
|
1183
|
+
if (isFinite(parsed) && parsed >= 1) setField('threads', parsed)
|
|
1184
|
+
}
|
|
1185
|
+
})),
|
|
1186
|
+
h(Row, { label: t('maxMb') },
|
|
1187
|
+
h('input', {
|
|
1188
|
+
className: 'dshdl-input',
|
|
1189
|
+
type: 'number',
|
|
1190
|
+
min: 1,
|
|
1191
|
+
value: value.maxDownloadMb,
|
|
1192
|
+
style: { width: '110px', minWidth: '110px' },
|
|
1193
|
+
onChange: function (e) {
|
|
1194
|
+
var parsed = Number(e.target.value)
|
|
1195
|
+
if (isFinite(parsed) && parsed >= 1) setField('maxDownloadMb', parsed)
|
|
1196
|
+
}
|
|
1197
|
+
})))
|
|
1198
|
+
|
|
1199
|
+
// --- 高级开关 ---
|
|
1200
|
+
var advancedToggle = h(Card, null,
|
|
1201
|
+
h(Row, { label: t('advanced'), hint: t('advancedHint') },
|
|
1202
|
+
h(Switch, {
|
|
1203
|
+
value: advanced,
|
|
1204
|
+
label: t('advanced'),
|
|
1205
|
+
onChange: function (next) {
|
|
1206
|
+
setAdvanced(next)
|
|
1207
|
+
writeStore(LS_ADVANCED, next ? '1' : '0')
|
|
1208
|
+
}
|
|
1209
|
+
})))
|
|
1210
|
+
|
|
1211
|
+
var advancedCards = []
|
|
1212
|
+
if (advanced) {
|
|
1213
|
+
advancedCards.push(h(Card, { key: 'adv-upstream', title: t('advUpstream') },
|
|
1214
|
+
h(TextRow, {
|
|
1215
|
+
label: t('proxyUrl'),
|
|
1216
|
+
hint: t('proxyUrlHint'),
|
|
1217
|
+
value: value.proxyUrl,
|
|
1218
|
+
placeholder: 'socks5://127.0.0.1:1080',
|
|
1219
|
+
onCommit: function (next) { setField('proxyUrl', next) }
|
|
1220
|
+
}),
|
|
1221
|
+
h(TextRow, {
|
|
1222
|
+
label: t('subscriptionUrl'),
|
|
1223
|
+
hint: t('subscriptionUrlHint'),
|
|
1224
|
+
value: value.subscriptionUrl,
|
|
1225
|
+
placeholder: 'https://…',
|
|
1226
|
+
onCommit: function (next) { setField('subscriptionUrl', next) }
|
|
1227
|
+
}),
|
|
1228
|
+
h(TextRow, {
|
|
1229
|
+
label: t('fetchProxyUrl'),
|
|
1230
|
+
hint: t('fetchProxyUrlHint'),
|
|
1231
|
+
value: value.fetchProxyUrl,
|
|
1232
|
+
placeholder: 'http://127.0.0.1:7890',
|
|
1233
|
+
onCommit: function (next) { setField('fetchProxyUrl', next) }
|
|
1234
|
+
}),
|
|
1235
|
+
h(SelectRow, {
|
|
1236
|
+
label: t('groupType'),
|
|
1237
|
+
value: value.groupType,
|
|
1238
|
+
options: [
|
|
1239
|
+
{ value: 'url-test', label: 'url-test' },
|
|
1240
|
+
{ value: 'select', label: 'select' },
|
|
1241
|
+
{ value: 'fallback', label: 'fallback' },
|
|
1242
|
+
{ value: 'load-balance', label: 'load-balance' }
|
|
1243
|
+
],
|
|
1244
|
+
onCommit: function (next) { setField('groupType', next) }
|
|
1245
|
+
}),
|
|
1246
|
+
h(TextRow, {
|
|
1247
|
+
label: t('preferredNode'),
|
|
1248
|
+
hint: t('preferredNodeHint'),
|
|
1249
|
+
value: value.preferredNode,
|
|
1250
|
+
onCommit: function (next) { setField('preferredNode', next) }
|
|
1251
|
+
}),
|
|
1252
|
+
h(Row, { label: t('autoUpdateHours') },
|
|
1253
|
+
h('input', {
|
|
1254
|
+
className: 'dshdl-input',
|
|
1255
|
+
type: 'number',
|
|
1256
|
+
min: 0,
|
|
1257
|
+
value: value.autoUpdateHours,
|
|
1258
|
+
style: { width: '90px', minWidth: '90px' },
|
|
1259
|
+
onChange: function (e) {
|
|
1260
|
+
var parsed = Number(e.target.value)
|
|
1261
|
+
if (isFinite(parsed) && parsed >= 0) setField('autoUpdateHours', parsed)
|
|
697
1262
|
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1263
|
+
})),
|
|
1264
|
+
h(TextRow, { label: t('latencyTestUrl'), value: value.latencyTestUrl, onCommit: function (next) { setField('latencyTestUrl', next) } }),
|
|
1265
|
+
h(Row, { label: t('latencyTimeoutMs') },
|
|
1266
|
+
h('input', {
|
|
1267
|
+
className: 'dshdl-input',
|
|
1268
|
+
type: 'number',
|
|
1269
|
+
min: 200,
|
|
1270
|
+
value: value.latencyTimeoutMs,
|
|
1271
|
+
style: { width: '100px', minWidth: '100px' },
|
|
1272
|
+
onChange: function (e) {
|
|
1273
|
+
var parsed = Number(e.target.value)
|
|
1274
|
+
if (isFinite(parsed) && parsed >= 200) setField('latencyTimeoutMs', parsed)
|
|
1275
|
+
}
|
|
1276
|
+
}))))
|
|
1277
|
+
|
|
1278
|
+
advancedCards.push(h(Card, { key: 'adv-routing', title: t('advRouting') },
|
|
1279
|
+
h(Row, { label: t('domesticDirect'), hint: t('domesticDirectHint') },
|
|
1280
|
+
h(Switch, { value: value.domesticDirect !== false, label: t('domesticDirect'), onChange: function (next) { setField('domesticDirect', next) } })),
|
|
1281
|
+
h(Row, { label: t('protectAiPlatforms'), hint: t('protectAiPlatformsHint') },
|
|
1282
|
+
h(Switch, { value: value.protectAiPlatforms !== false, label: t('protectAiPlatforms'), onChange: function (next) { setField('protectAiPlatforms', next) } })),
|
|
1283
|
+
h(ListRow, {
|
|
1284
|
+
label: t('extraDirectDomains'),
|
|
1285
|
+
hint: t('extraDirectDomainsHint'),
|
|
1286
|
+
value: value.extraDirectDomains,
|
|
1287
|
+
onCommit: function (next) { setField('extraDirectDomains', next) }
|
|
1288
|
+
}),
|
|
1289
|
+
h(ListRow, { label: t('extraRules'), hint: t('extraRulesHint'), value: value.extraRules, onCommit: function (next) { setField('extraRules', next) } }),
|
|
1290
|
+
h(ListRow, { label: t('excludeRules'), hint: t('excludeRulesHint'), value: value.excludeRules, onCommit: function (next) { setField('excludeRules', next) } })))
|
|
1291
|
+
|
|
1292
|
+
advancedCards.push(h(Card, { key: 'adv-engine', title: t('advEngine') },
|
|
1293
|
+
h(Row, { label: t('maxRedirects') },
|
|
1294
|
+
h('input', {
|
|
1295
|
+
className: 'dshdl-input',
|
|
1296
|
+
type: 'number',
|
|
1297
|
+
min: 0,
|
|
1298
|
+
max: 30,
|
|
1299
|
+
value: value.maxRedirects,
|
|
1300
|
+
style: { width: '80px', minWidth: '80px' },
|
|
1301
|
+
onChange: function (e) {
|
|
1302
|
+
var parsed = Number(e.target.value)
|
|
1303
|
+
if (isFinite(parsed) && parsed >= 0) setField('maxRedirects', parsed)
|
|
1304
|
+
}
|
|
1305
|
+
})),
|
|
1306
|
+
h(Row, { label: t('stallTimeoutS') },
|
|
1307
|
+
h('input', {
|
|
1308
|
+
className: 'dshdl-input',
|
|
1309
|
+
type: 'number',
|
|
1310
|
+
min: 5,
|
|
1311
|
+
value: value.stallTimeoutS,
|
|
1312
|
+
style: { width: '80px', minWidth: '80px' },
|
|
1313
|
+
onChange: function (e) {
|
|
1314
|
+
var parsed = Number(e.target.value)
|
|
1315
|
+
if (isFinite(parsed) && parsed >= 5) setField('stallTimeoutS', parsed)
|
|
705
1316
|
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1317
|
+
})),
|
|
1318
|
+
h(Row, { label: t('downloadTimeoutS') },
|
|
1319
|
+
h('input', {
|
|
1320
|
+
className: 'dshdl-input',
|
|
1321
|
+
type: 'number',
|
|
1322
|
+
min: 1,
|
|
1323
|
+
value: value.downloadTimeoutS,
|
|
1324
|
+
style: { width: '90px', minWidth: '90px' },
|
|
1325
|
+
onChange: function (e) {
|
|
1326
|
+
var parsed = Number(e.target.value)
|
|
1327
|
+
if (isFinite(parsed) && parsed >= 1) setField('downloadTimeoutS', parsed)
|
|
1328
|
+
}
|
|
1329
|
+
})),
|
|
1330
|
+
h(Row, { label: t('insecureTls'), hint: t('insecureTlsHint') },
|
|
1331
|
+
h(Switch, { value: value.insecureTls === true, label: t('insecureTls'), onChange: function (next) { setField('insecureTls', next) } })),
|
|
1332
|
+
h(Row, { label: t('allowOutsideWorkspace'), hint: t('allowOutsideWorkspaceHint') },
|
|
1333
|
+
h(Switch, { value: value.allowOutsideWorkspace === true, label: t('allowOutsideWorkspace'), onChange: function (next) { setField('allowOutsideWorkspace', next) } })),
|
|
1334
|
+
h(Row, { label: t('guardShellDownloads'), hint: t('guardShellDownloadsHint') },
|
|
1335
|
+
h(Switch, { value: value.guardShellDownloads !== false, label: t('guardShellDownloads'), onChange: function (next) { setField('guardShellDownloads', next) } }))))
|
|
1336
|
+
|
|
1337
|
+
advancedCards.push(h(Card, { key: 'adv-harness', title: t('advHarness') },
|
|
1338
|
+
h(Row, { label: t('harnessProxy'), hint: t('harnessProxyHint') },
|
|
1339
|
+
h(Switch, { value: value.harnessProxy === true, label: t('harnessProxy'), onChange: function (next) { setField('harnessProxy', next) } })),
|
|
1340
|
+
h(Row, { label: t('harnessProxyPort'), hint: t('harnessProxyPortHint') },
|
|
1341
|
+
h('input', {
|
|
1342
|
+
className: 'dshdl-input',
|
|
1343
|
+
type: 'number',
|
|
1344
|
+
min: 1024,
|
|
1345
|
+
max: 65535,
|
|
1346
|
+
value: value.harnessProxyPort,
|
|
1347
|
+
style: { width: '100px', minWidth: '100px' },
|
|
1348
|
+
onChange: function (e) {
|
|
1349
|
+
var parsed = Number(e.target.value)
|
|
1350
|
+
if (isFinite(parsed) && parsed >= 1024 && parsed <= 65535) setField('harnessProxyPort', parsed)
|
|
1351
|
+
}
|
|
1352
|
+
}))))
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// --- 会话保护 ---
|
|
1356
|
+
var protectedTotal = (status && Array.isArray(status.protectedDomains)) ? status.protectedDomains.length : 0
|
|
1357
|
+
var guardMissingCount = (guard && Array.isArray(guard.missing)) ? guard.missing.length : 0
|
|
1358
|
+
var guardCard = h(Card, { title: t('guard'), hint: t('guardHint') },
|
|
1359
|
+
guard
|
|
1360
|
+
? h(React.Fragment, null,
|
|
1361
|
+
h(Row, {
|
|
1362
|
+
label: t('guard'),
|
|
1363
|
+
hint: guardMissingCount === 0
|
|
1364
|
+
? t('guardCovered', protectedTotal)
|
|
1365
|
+
: t('guardMissing', guardMissingCount)
|
|
1366
|
+
},
|
|
1367
|
+
h('div', { style: { display: 'flex', gap: '8px' } },
|
|
1368
|
+
h(Btn, { variant: 'primary', onClick: applyGuard, disabled: busy === 'guard' }, t('guardApply')),
|
|
1369
|
+
h(Btn, { onClick: restoreGuard, disabled: busy === 'guard-restore' }, t('guardRestore')))),
|
|
1370
|
+
(guard.proxy_vars && guard.proxy_vars.length === 0)
|
|
1371
|
+
? h('div', { className: 'dshdl-note' }, t('guardNoVars'))
|
|
1372
|
+
: null)
|
|
1373
|
+
: h('div', { className: 'dshdl-note' }, '—'))
|
|
1374
|
+
|
|
1375
|
+
// --- 流量 ---
|
|
1376
|
+
var up = traffic ? Number(traffic.up) || 0 : 0
|
|
1377
|
+
var down = traffic ? Number(traffic.down) || 0 : 0
|
|
1378
|
+
var upRate = traffic ? Number(traffic.upRate) || 0 : 0
|
|
1379
|
+
var downRate = traffic ? Number(traffic.downRate) || 0 : 0
|
|
1380
|
+
var trafficCard = h(Card, { title: t('traffic') },
|
|
1381
|
+
traffic && traffic.active !== false
|
|
1382
|
+
? h('div', { className: 'dshdl-traffic' },
|
|
1383
|
+
h('div', { className: 'dshdl-tItem' },
|
|
1384
|
+
h('div', { className: 'dshdl-tLabel' }, '↑ ' + t('trafficUp')),
|
|
1385
|
+
h('div', { className: 'dshdl-tValue' }, formatRate(upRate)),
|
|
1386
|
+
h('div', { className: 'dshdl-tSum' }, t('trafficTotal') + ' ' + formatBytes(up))),
|
|
1387
|
+
h('div', { className: 'dshdl-tItem' },
|
|
1388
|
+
h('div', { className: 'dshdl-tLabel' }, '↓ ' + t('trafficDown')),
|
|
1389
|
+
h('div', { className: 'dshdl-tValue' }, formatRate(downRate)),
|
|
1390
|
+
h('div', { className: 'dshdl-tSum' }, t('trafficTotal') + ' ' + formatBytes(down))))
|
|
1391
|
+
: h('div', { className: 'dshdl-note' }, t('trafficIdle')))
|
|
1392
|
+
|
|
1393
|
+
var aboutCard = status
|
|
1394
|
+
? h(Card, { title: t('about') },
|
|
1395
|
+
h('dl', { className: 'dshdl-kv' },
|
|
1396
|
+
h('dt', null, t('dataDir')),
|
|
1397
|
+
h('dd', null, status.dataDir || '—')),
|
|
1398
|
+
h('p', { className: 'dshdl-cardHint', style: { marginTop: '10px', marginBottom: 0 } }, t('scopeNote')))
|
|
1399
|
+
: null
|
|
1400
|
+
|
|
1401
|
+
return h('div', { className: cls('dshdl-root', compact ? 'dshdl-compact' : '') },
|
|
1402
|
+
header,
|
|
1403
|
+
errorNode,
|
|
1404
|
+
actionErrorNode,
|
|
1405
|
+
noticeNode,
|
|
1406
|
+
statusCard,
|
|
1407
|
+
profilesCard,
|
|
1408
|
+
nodesCard,
|
|
1409
|
+
downloadCard,
|
|
1410
|
+
advancedToggle,
|
|
1411
|
+
advancedCards,
|
|
1412
|
+
guardCard,
|
|
1413
|
+
trafficCard,
|
|
1414
|
+
aboutCard)
|
|
736
1415
|
}
|
|
737
1416
|
|
|
738
1417
|
// ------------------------------------------------------------------
|
|
739
|
-
//
|
|
1418
|
+
// 侧栏导航图标(shell 只画这一格,行、标签、高亮由 shell 负责)
|
|
1419
|
+
// ------------------------------------------------------------------
|
|
1420
|
+
function PanelIcon(props) {
|
|
1421
|
+
var size = props.size || 16
|
|
1422
|
+
return h('svg', {
|
|
1423
|
+
'data-dsh-panel-entry': PANEL_ID,
|
|
1424
|
+
viewBox: '0 0 16 16',
|
|
1425
|
+
width: size,
|
|
1426
|
+
height: size,
|
|
1427
|
+
fill: 'none',
|
|
1428
|
+
stroke: 'currentColor',
|
|
1429
|
+
strokeWidth: 1.3,
|
|
1430
|
+
strokeLinecap: 'round',
|
|
1431
|
+
strokeLinejoin: 'round',
|
|
1432
|
+
'aria-hidden': 'true'
|
|
1433
|
+
},
|
|
1434
|
+
h('path', { d: 'M8 2v7.2' }),
|
|
1435
|
+
h('path', { d: 'M5.2 6.4 8 9.2l2.8-2.8' }),
|
|
1436
|
+
h('path', { d: 'M2.6 11.4v1.4c0 .6.5 1.1 1.1 1.1h8.6c.6 0 1.1-.5 1.1-1.1v-1.4' }))
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// ------------------------------------------------------------------
|
|
1440
|
+
// 页脚流量徽标
|
|
740
1441
|
// ------------------------------------------------------------------
|
|
741
|
-
/**
|
|
742
|
-
* 实时显示经过本插件内核的上下行速率与累计流量。
|
|
743
|
-
*
|
|
744
|
-
* 数据来自 `/traffic`(内核的字节计数器)。这个插槽同时服务展开的侧边栏与
|
|
745
|
-
* 56px 收起栏,`props.wide` 决定用哪种排版;收起态只放得下一个速率。
|
|
746
|
-
* 页面隐藏时降到 5 秒一次,避免后台空转。
|
|
747
|
-
*/
|
|
748
1442
|
function TrafficBadge(props) {
|
|
749
1443
|
var wide = props.wide !== false
|
|
750
1444
|
var t = props.t || function (key) { return key }
|
|
751
|
-
var
|
|
752
|
-
|
|
753
|
-
var setTraffic = state[1]
|
|
754
|
-
var failedState = React.useState(false)
|
|
755
|
-
var failed = failedState[0]
|
|
756
|
-
var setFailed = failedState[1]
|
|
757
|
-
var alive = React.useRef(true)
|
|
758
|
-
|
|
759
|
-
React.useEffect(function () {
|
|
760
|
-
alive.current = true
|
|
761
|
-
var timer = null
|
|
762
|
-
function tick() {
|
|
763
|
-
apiFetch('traffic')
|
|
764
|
-
.then(function (payload) { if (alive.current) { setTraffic(payload); setFailed(false) } })
|
|
765
|
-
.catch(function () { if (alive.current) setFailed(true) })
|
|
766
|
-
}
|
|
767
|
-
function schedule() {
|
|
768
|
-
timer = setTimeout(function () { tick(); schedule() }, document.hidden ? 5000 : 1500)
|
|
769
|
-
}
|
|
770
|
-
tick()
|
|
771
|
-
schedule()
|
|
772
|
-
return function () { alive.current = false; if (timer) clearTimeout(timer) }
|
|
773
|
-
}, [])
|
|
774
|
-
|
|
775
|
-
// 内核没跑或后端不可达时不占地方。
|
|
776
|
-
if (failed || traffic === null || traffic.active === false) return null
|
|
777
|
-
|
|
778
|
-
var up = Number(traffic.up) || 0
|
|
779
|
-
var down = Number(traffic.down) || 0
|
|
1445
|
+
var traffic = useTraffic(1500)
|
|
1446
|
+
if (traffic === null || traffic.active === false) return null
|
|
780
1447
|
var upRate = Number(traffic.upRate) || 0
|
|
781
1448
|
var downRate = Number(traffic.downRate) || 0
|
|
782
|
-
var tip = t('
|
|
783
|
-
t('trafficUp') + '
|
|
784
|
-
t('trafficDown') + '
|
|
785
|
-
|
|
1449
|
+
var tip = t('traffic') + '\n' +
|
|
1450
|
+
t('trafficUp') + ': ' + formatRate(upRate) + ' (' + t('trafficTotal') + ' ' + formatBytes(traffic.up) + ')\n' +
|
|
1451
|
+
t('trafficDown') + ': ' + formatRate(downRate) + ' (' + t('trafficTotal') + ' ' + formatBytes(traffic.down) + ')'
|
|
786
1452
|
if (!wide) {
|
|
787
|
-
return
|
|
788
|
-
|
|
789
|
-
|
|
1453
|
+
return h('div', { className: 'dshdl-trafRail', title: tip },
|
|
1454
|
+
h('span', null, '↓'),
|
|
1455
|
+
h('span', null, formatRateTiny(downRate)))
|
|
790
1456
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
React.createElement('span', { className: 'dshdl-trafArrow up' }, '↑'),
|
|
795
|
-
React.createElement('span', { className: 'dshdl-trafRate' }, formatRate(upRate)),
|
|
796
|
-
React.createElement('span', { className: 'dshdl-trafSum' }, formatBytes(up))),
|
|
797
|
-
React.createElement('div', { className: 'dshdl-trafRow' },
|
|
798
|
-
React.createElement('span', { className: 'dshdl-trafArrow down' }, '↓'),
|
|
799
|
-
React.createElement('span', { className: 'dshdl-trafRate' }, formatRate(downRate)),
|
|
800
|
-
React.createElement('span', { className: 'dshdl-trafSum' }, formatBytes(down))))
|
|
1457
|
+
return h('div', { className: 'dshdl-trafFoot', title: tip },
|
|
1458
|
+
h('div', { className: 'row' }, h('span', null, '↑'), h('span', null, formatRateTiny(upRate))),
|
|
1459
|
+
h('div', { className: 'row' }, h('span', null, '↓'), h('span', null, formatRateTiny(downRate))))
|
|
801
1460
|
}
|
|
802
1461
|
|
|
803
1462
|
// ------------------------------------------------------------------
|
|
@@ -810,11 +1469,13 @@ window.__ModuleLoader__.load({
|
|
|
810
1469
|
function apply(ctx) {
|
|
811
1470
|
ensureCss()
|
|
812
1471
|
|
|
813
|
-
var
|
|
1472
|
+
var ctxRef = { current: ctx }
|
|
1473
|
+
var locale = localeService(ctx)
|
|
1474
|
+
|
|
1475
|
+
// 字典注册失败不影响面板(下面有自己的回退字典)。
|
|
814
1476
|
try {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
} catch (e) { /* 字典注册失败不影响面板 */ }
|
|
1477
|
+
if (locale) locale.register(LOCALE_NS, { zh: LOCALE_ZH, en: LOCALE_EN })
|
|
1478
|
+
} catch (e) { /* ignore */ }
|
|
818
1479
|
|
|
819
1480
|
var cached = null
|
|
820
1481
|
function getConfigForm() {
|
|
@@ -826,30 +1487,62 @@ window.__ModuleLoader__.load({
|
|
|
826
1487
|
return cached
|
|
827
1488
|
}
|
|
828
1489
|
|
|
1490
|
+
function label() {
|
|
1491
|
+
try {
|
|
1492
|
+
if (locale) return locale.bind(LOCALE_NS)('entryLabel')
|
|
1493
|
+
} catch (e) { /* ignore */ }
|
|
1494
|
+
return '下载代理'
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// 1) 左侧栏导航行(shell 拥有按钮/标签/高亮/收起态几何)
|
|
1498
|
+
ctx.slots.inject('sidebar.panellist', function () {
|
|
1499
|
+
return ctx.slots.register({
|
|
1500
|
+
name: 'sidebar.panellist',
|
|
1501
|
+
id: PANEL_ID,
|
|
1502
|
+
order: 30,
|
|
1503
|
+
label: label
|
|
1504
|
+
}, PanelIcon)
|
|
1505
|
+
})
|
|
1506
|
+
|
|
1507
|
+
// 2) 左侧栏对应的主面板页面
|
|
1508
|
+
ctx.slots.inject('main', function () {
|
|
1509
|
+
return ctx.slots.register({
|
|
1510
|
+
name: 'main',
|
|
1511
|
+
key: PANEL_ID,
|
|
1512
|
+
label: label,
|
|
1513
|
+
inject: function () {
|
|
1514
|
+
return { ctxRef: ctxRef, scope: getConfigForm(), variant: 'panel' }
|
|
1515
|
+
}
|
|
1516
|
+
}, DownloaderPage)
|
|
1517
|
+
})
|
|
1518
|
+
|
|
1519
|
+
// 3) 设置页入口 —— 同一套 Page 组件,只是排版更紧凑
|
|
829
1520
|
ctx.slots.inject('settings.section', function () {
|
|
830
1521
|
return ctx.slots.register({
|
|
831
1522
|
name: 'settings.section',
|
|
832
1523
|
id: ENTRY_ID,
|
|
833
1524
|
order: 91,
|
|
834
|
-
label:
|
|
1525
|
+
label: label,
|
|
835
1526
|
locale: LOCALE_NS,
|
|
836
1527
|
inject: function () {
|
|
837
|
-
return {
|
|
838
|
-
scope: getConfigForm(),
|
|
839
|
-
t: function (key) { return translate ? translate(key) : key }
|
|
840
|
-
}
|
|
1528
|
+
return { ctxRef: ctxRef, scope: getConfigForm(), variant: 'settings' }
|
|
841
1529
|
}
|
|
842
|
-
},
|
|
1530
|
+
}, DownloaderPage)
|
|
843
1531
|
})
|
|
844
1532
|
|
|
845
|
-
//
|
|
1533
|
+
// 4) 侧栏页脚:实时流量。设置页/面板没打开也能看
|
|
846
1534
|
ctx.slots.inject('sidebar.footer.action', function () {
|
|
847
1535
|
return ctx.slots.register({
|
|
848
1536
|
name: 'sidebar.footer.action',
|
|
849
1537
|
id: ENTRY_ID + '-traffic',
|
|
850
1538
|
order: 20,
|
|
851
1539
|
inject: function () {
|
|
852
|
-
|
|
1540
|
+
var translate = function (key) { return key }
|
|
1541
|
+
try {
|
|
1542
|
+
var service = localeService(ctx)
|
|
1543
|
+
if (service) translate = service.bind(LOCALE_NS)
|
|
1544
|
+
} catch (e) { /* ignore */ }
|
|
1545
|
+
return { t: translate }
|
|
853
1546
|
}
|
|
854
1547
|
}, TrafficBadge)
|
|
855
1548
|
})
|