@having5548/dsh-downloader 0.0.0-stage → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +376 -0
- package/LICENSE +29 -0
- package/README.en.md +378 -0
- package/README.md +382 -2
- package/cordis.patch.yml +9 -0
- package/lib/client.js +862 -0
- package/lib/core/ai-domains.js +130 -0
- package/lib/core/data/cn-cidrs.json.gz +0 -0
- package/lib/core/data/cn-cidrs6.json.gz +0 -0
- package/lib/core/data/cn-domains.json.gz +0 -0
- package/lib/core/profiles.js +372 -0
- package/lib/core/proxy-server.js +341 -0
- package/lib/core/rules.js +242 -0
- package/lib/core/session-guard.js +208 -0
- package/lib/core/share-links.js +371 -0
- package/lib/core/shell-proxy.js +143 -0
- package/lib/core/subscription.js +126 -0
- package/lib/core/transports.js +664 -0
- package/lib/core/upstream.js +1044 -0
- package/lib/core/vmess.js +298 -0
- package/lib/core/ws.js +128 -0
- package/lib/download/auth.js +110 -0
- package/lib/download/fetch-file.js +363 -0
- package/lib/download/http.js +299 -0
- package/lib/download/net-probe.js +126 -0
- package/lib/download/route.js +103 -0
- package/lib/download/segmented.js +279 -0
- package/lib/download/util.js +103 -0
- package/lib/index.js +1199 -0
- package/lib/native/connector.exe +0 -0
- package/native/README.md +59 -0
- package/native/dial.go +17 -0
- package/native/frame.go +112 -0
- package/native/go.mod +19 -0
- package/native/go.sum +32 -0
- package/native/hysteria2.go +178 -0
- package/native/main.go +79 -0
- package/native/reality.go +188 -0
- package/native/salamander.go +84 -0
- package/native/vless.go +455 -0
- package/package.json +86 -4
package/lib/client.js
ADDED
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// dsh-downloader —— DSH 下载代理插件(浏览器/客户端半部)
|
|
3
|
+
//
|
|
4
|
+
// 职责:在「设置 → 下载代理」里挂一个管理面板
|
|
5
|
+
// 1. 编辑插件 Config(经 ctx.get('configForms').get('dsh-downloader'))
|
|
6
|
+
// 2. 轮询 /dsh-downloader/status 与 /proxies 显示实时状态
|
|
7
|
+
// 3. 更新订阅 / 重启内核 / 逐节点测速与选择
|
|
8
|
+
//
|
|
9
|
+
// 以预打包 bundle 形式随插件安装,格式与官方 client 插件一致
|
|
10
|
+
// (window.__ModuleLoader__)。注意:0.1.7 起 ctx.settingsScope 已删除,
|
|
11
|
+
// 必须惰性走 ctx.configForms;且 scope.subscribe / getSnapshot 是类原型方法,
|
|
12
|
+
// 直接传给 useSyncExternalStore 会因 this 丢失而崩,必须包一层闭包。
|
|
13
|
+
// ============================================================================
|
|
14
|
+
window.__ModuleLoader__.load({
|
|
15
|
+
id: '@having5548/dsh-downloader',
|
|
16
|
+
factory: function (require) {
|
|
17
|
+
var module = { exports: {} }
|
|
18
|
+
var exports = module.exports
|
|
19
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
20
|
+
|
|
21
|
+
var React = require('react')
|
|
22
|
+
|
|
23
|
+
// ------------------------------------------------------------------
|
|
24
|
+
// 常量
|
|
25
|
+
// ------------------------------------------------------------------
|
|
26
|
+
var ENTRY_ID = 'dsh-downloader'
|
|
27
|
+
var LOCALE_NS = 'dsh-downloader'
|
|
28
|
+
var API_BASE = '/dsh-downloader/'
|
|
29
|
+
|
|
30
|
+
var DEFAULTS = {
|
|
31
|
+
enabled: true,
|
|
32
|
+
proxyUrl: '',
|
|
33
|
+
subscriptionUrl: '',
|
|
34
|
+
fetchProxyUrl: '',
|
|
35
|
+
autoUpdateHours: 24,
|
|
36
|
+
groupType: 'url-test',
|
|
37
|
+
preferredNode: '',
|
|
38
|
+
latencyTestUrl: 'http://www.gstatic.com/generate_204',
|
|
39
|
+
latencyTimeoutMs: 3000,
|
|
40
|
+
domesticDirect: true,
|
|
41
|
+
protectAiPlatforms: true,
|
|
42
|
+
extraDirectDomains: [],
|
|
43
|
+
guardShellDownloads: true,
|
|
44
|
+
extraRules: [],
|
|
45
|
+
excludeRules: [],
|
|
46
|
+
downloadDir: '',
|
|
47
|
+
maxDownloadMb: 512,
|
|
48
|
+
downloadTimeoutS: 600,
|
|
49
|
+
stallTimeoutS: 30,
|
|
50
|
+
threads: 4,
|
|
51
|
+
insecureTls: false,
|
|
52
|
+
allowOutsideWorkspace: false,
|
|
53
|
+
maxRedirects: 10
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
var LOCALE_ZH = {
|
|
57
|
+
nav: '下载代理',
|
|
58
|
+
title: '下载代理',
|
|
59
|
+
intro: '让模型下载境外文件时自动走代理、境内直连。代理出口由插件自带的订阅内核提供,不依赖本机 Clash。',
|
|
60
|
+
upstream: '上游',
|
|
61
|
+
nodes: '节点',
|
|
62
|
+
rules: '分流规则',
|
|
63
|
+
download: '下载',
|
|
64
|
+
refresh: '刷新',
|
|
65
|
+
restart: '重启内核',
|
|
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: '未运行',
|
|
80
|
+
stRunning: '运行中',
|
|
81
|
+
stDisabled: '已关闭',
|
|
82
|
+
stNoSubscription: '无可用订阅',
|
|
83
|
+
stFailed: '失败',
|
|
84
|
+
stIdle: '未启动',
|
|
85
|
+
mdExplicit: '上游:配置的 proxyUrl',
|
|
86
|
+
mdEnvironment: '上游:启动环境代理',
|
|
87
|
+
mdSubscription: '上游:自包含订阅',
|
|
88
|
+
mdNone: '上游:未配置',
|
|
89
|
+
nodesBadge: '节点',
|
|
90
|
+
profiles: '节点配置',
|
|
91
|
+
profilesIntro: '一份配置 = 一条订阅地址或一个导入的配置文件,带自己的名字、上次更新时间与自动更新间隔(对齐 FlClash 的配置模型)。',
|
|
92
|
+
importUrl: '从订阅地址导入',
|
|
93
|
+
importUrlPlaceholder: 'https://… 或 clash://install-config?url=…',
|
|
94
|
+
importLabelPlaceholder: '名称(留空则自动取响应头文件名,再退回域名)',
|
|
95
|
+
importFile: '从文件导入',
|
|
96
|
+
importBtn: '导入',
|
|
97
|
+
noProfiles: '还没有导入任何配置。可以从订阅地址或本地文件导入。',
|
|
98
|
+
profileCurrent: '当前',
|
|
99
|
+
profileFile: '本地文件',
|
|
100
|
+
profileAuto: '自动更新',
|
|
101
|
+
profileSelect: '选用',
|
|
102
|
+
profileUpdate: '更新',
|
|
103
|
+
profileRemove: '删除',
|
|
104
|
+
scopeTitle: '代理作用域',
|
|
105
|
+
scopeIntro: '只服务 DSH 自己发起的下载请求:内核只绑 127.0.0.1 并要求本进程的随机令牌,其它程序连不上也借不走;系统代理与环境变量从未被修改。',
|
|
106
|
+
trafficTitle: '经过本插件内核的实时流量',
|
|
107
|
+
trafficUp: '上行',
|
|
108
|
+
trafficDown: '下行',
|
|
109
|
+
trafficTotal: '累计',
|
|
110
|
+
unavailable: '当前环境无法持久化设置,本次修改仅当前页面生效。'
|
|
111
|
+
}
|
|
112
|
+
var LOCALE_EN = {
|
|
113
|
+
nav: 'Download proxy',
|
|
114
|
+
title: 'Download proxy',
|
|
115
|
+
intro: 'Routes foreign downloads through a proxy and keeps domestic targets direct. The proxy exit comes from the plugin\'s own subscription core — no local Clash needed.',
|
|
116
|
+
upstream: 'Upstream',
|
|
117
|
+
nodes: 'Nodes',
|
|
118
|
+
rules: 'Rules',
|
|
119
|
+
download: 'Download',
|
|
120
|
+
refresh: 'Refresh',
|
|
121
|
+
restart: 'Restart core',
|
|
122
|
+
updateSubscription: 'Update subscription',
|
|
123
|
+
testAll: 'Test all',
|
|
124
|
+
select: 'Select',
|
|
125
|
+
test: 'Test',
|
|
126
|
+
clearLatency: 'Clear latencies',
|
|
127
|
+
sessionGuard: 'Session guard',
|
|
128
|
+
guardIntro: 'Writes the Chinese AI platform domains into NO_PROXY in $DSH_HOME/.env so model connections bypass the global proxy — closing the proxy app or losing a node cannot break the session. The file is backed up before any write.',
|
|
129
|
+
guardApply: 'Write NO_PROXY',
|
|
130
|
+
guardRestore: 'Restore backup',
|
|
131
|
+
guardCovered: 'covered',
|
|
132
|
+
guardMissing: 'missing',
|
|
133
|
+
guardNoProxyVars: 'That file sets no HTTP_PROXY / HTTPS_PROXY, so this entry is inert for now',
|
|
134
|
+
guardRestart: 'Restart DSH for the change to take effect',
|
|
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',
|
|
146
|
+
profiles: 'Node profiles',
|
|
147
|
+
profilesIntro: 'One profile is either a subscription URL or an imported file, each with its own label, last-update time and auto-update interval — the same model FlClash uses.',
|
|
148
|
+
importUrl: 'Import from a subscription URL',
|
|
149
|
+
importUrlPlaceholder: 'https://… or clash://install-config?url=…',
|
|
150
|
+
importLabelPlaceholder: 'Name (blank: use the response filename, then the host)',
|
|
151
|
+
importFile: 'Import from a file',
|
|
152
|
+
importBtn: 'Import',
|
|
153
|
+
noProfiles: 'No profiles yet. Import one from a subscription URL or a local file.',
|
|
154
|
+
profileCurrent: 'current',
|
|
155
|
+
profileFile: 'local file',
|
|
156
|
+
profileAuto: 'auto-update',
|
|
157
|
+
profileSelect: 'Use',
|
|
158
|
+
profileUpdate: 'Update',
|
|
159
|
+
profileRemove: 'Delete',
|
|
160
|
+
scopeTitle: 'Proxy scope',
|
|
161
|
+
scopeIntro: 'Serves only downloads this plugin starts: the core binds 127.0.0.1 and requires a per-process random token, so no other program can reach or borrow it. The system proxy and the environment are never modified.',
|
|
162
|
+
trafficTitle: 'Live traffic through this plugin’s rule core',
|
|
163
|
+
trafficUp: 'Up',
|
|
164
|
+
trafficDown: 'Down',
|
|
165
|
+
trafficTotal: 'total',
|
|
166
|
+
unavailable: 'Settings cannot be persisted in this environment; changes apply to this page only.'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ------------------------------------------------------------------
|
|
170
|
+
// 工具函数
|
|
171
|
+
// ------------------------------------------------------------------
|
|
172
|
+
function formatBytes(value) {
|
|
173
|
+
var n = Number(value) || 0
|
|
174
|
+
var units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
175
|
+
var i = 0
|
|
176
|
+
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++ }
|
|
177
|
+
return (i === 0 ? n : n.toFixed(n >= 100 ? 0 : n >= 10 ? 1 : 2)) + ' ' + units[i]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function formatTime(ts) {
|
|
181
|
+
if (ts === null || ts === undefined) return '—'
|
|
182
|
+
try { return new Date(ts).toLocaleString() } catch (e) { return String(ts) }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 速率:在 formatBytes 后面加 /s。 */
|
|
186
|
+
function formatRate(bytesPerSecond) {
|
|
187
|
+
return formatBytes(bytesPerSecond) + '/s'
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 收起栏(56px)用的极简写法:12.4M / 980K。 */
|
|
191
|
+
function formatRateTiny(bytesPerSecond) {
|
|
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'
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 与后端同源的请求封装(面板内外都要用,所以放在模块层)。 */
|
|
200
|
+
function apiFetch(path, body) {
|
|
201
|
+
return fetch(API_BASE + path, {
|
|
202
|
+
method: body === undefined ? 'GET' : 'POST',
|
|
203
|
+
headers: body === undefined ? undefined : { 'content-type': 'application/json' },
|
|
204
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
205
|
+
}).then(function (response) {
|
|
206
|
+
return response.text().then(function (text) {
|
|
207
|
+
var payload = text.length > 0 ? JSON.parse(text) : {}
|
|
208
|
+
if (!response.ok) throw new Error(payload.error || ('HTTP ' + response.status))
|
|
209
|
+
return payload
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function linesToArray(text) {
|
|
215
|
+
return String(text || '')
|
|
216
|
+
.split(/\r?\n/)
|
|
217
|
+
.map(function (line) { return line.trim() })
|
|
218
|
+
.filter(function (line) { return line.length > 0 })
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function arrayToLines(list) {
|
|
222
|
+
return Array.isArray(list) ? list.join('\n') : ''
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 状态 / 上游模式的展示名(原始值仍是英文标识符,只翻译给人看的那一份)
|
|
226
|
+
var STATE_KEYS = { running: 'stRunning', disabled: 'stDisabled', 'no-subscription': 'stNoSubscription', failed: 'stFailed', idle: 'stIdle' }
|
|
227
|
+
var MODE_KEYS = { explicit: 'mdExplicit', environment: 'mdEnvironment', subscription: 'mdSubscription', none: 'mdNone' }
|
|
228
|
+
|
|
229
|
+
function labelOf(map, value, translate) {
|
|
230
|
+
var key = map[value]
|
|
231
|
+
return key ? translate(key) : (value || '?')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ------------------------------------------------------------------
|
|
235
|
+
// 样式(沿用 DSH 语义化 CSS 变量,自动适配明暗主题)
|
|
236
|
+
// ------------------------------------------------------------------
|
|
237
|
+
var CSS = [
|
|
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('')
|
|
282
|
+
|
|
283
|
+
function ensureCss() {
|
|
284
|
+
try {
|
|
285
|
+
if (document.getElementById('dsh-downloader-style')) return
|
|
286
|
+
var el = document.createElement('style')
|
|
287
|
+
el.id = 'dsh-downloader-style'
|
|
288
|
+
el.textContent = CSS
|
|
289
|
+
document.head.appendChild(el)
|
|
290
|
+
} catch (e) { /* ignore */ }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ------------------------------------------------------------------
|
|
294
|
+
// 面板
|
|
295
|
+
// ------------------------------------------------------------------
|
|
296
|
+
function Field(props) {
|
|
297
|
+
var label = props.label
|
|
298
|
+
var hint = props.hint
|
|
299
|
+
var value = props.value
|
|
300
|
+
var onChange = props.onChange
|
|
301
|
+
var onCommit = props.onCommit
|
|
302
|
+
var type = props.type || 'text'
|
|
303
|
+
var disabled = !!props.disabled
|
|
304
|
+
|
|
305
|
+
var control
|
|
306
|
+
if (type === 'bool') {
|
|
307
|
+
control = React.createElement('input', {
|
|
308
|
+
className: 'dshdl-check',
|
|
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) }
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
return React.createElement('div', { className: 'dshdl-row' },
|
|
345
|
+
React.createElement('span', { className: 'dshdl-label' }, label),
|
|
346
|
+
control,
|
|
347
|
+
hint ? React.createElement('span', { className: 'dshdl-hint' }, hint) : null)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function DownloaderSection(props) {
|
|
351
|
+
var scope = props.scope
|
|
352
|
+
var t = props.t || function (key) { return key }
|
|
353
|
+
|
|
354
|
+
// scope.subscribe / getSnapshot 是类原型方法:必须包一层闭包保住 this。
|
|
355
|
+
var snap = scope
|
|
356
|
+
? React.useSyncExternalStore(
|
|
357
|
+
function (listener) { return scope.subscribe(listener) },
|
|
358
|
+
function () { return scope.getSnapshot() })
|
|
359
|
+
: { status: 'unavailable', value: undefined }
|
|
360
|
+
|
|
361
|
+
var stored = Object.assign({}, DEFAULTS, (snap && snap.value) || {})
|
|
362
|
+
var [draft, setDraft] = React.useState({})
|
|
363
|
+
var [status, setStatus] = React.useState(null)
|
|
364
|
+
var [nodes, setNodes] = React.useState([])
|
|
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
|
+
|
|
372
|
+
var value = Object.assign({}, stored, draft)
|
|
373
|
+
|
|
374
|
+
function set(field, next) {
|
|
375
|
+
setDraft(function (prev) { var copy = Object.assign({}, prev); copy[field] = next; return copy })
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function commit(field, next) {
|
|
379
|
+
if (!scope) return
|
|
380
|
+
setDraft(function (prev) { var copy = Object.assign({}, prev); delete copy[field]; return copy })
|
|
381
|
+
try {
|
|
382
|
+
var promise = scope.set(field, next)
|
|
383
|
+
if (promise && typeof promise.catch === 'function') {
|
|
384
|
+
promise.catch(function (e) { setError(String((e && e.message) || e)) })
|
|
385
|
+
}
|
|
386
|
+
} catch (e) {
|
|
387
|
+
setError(String((e && e.message) || e))
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function commitBool(field, next) {
|
|
392
|
+
set(field, next)
|
|
393
|
+
commit(field, next)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function commitNumber(field, raw) {
|
|
397
|
+
var parsed = Number(raw)
|
|
398
|
+
if (!isFinite(parsed)) { commit(field, stored[field]); return }
|
|
399
|
+
commit(field, parsed)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
var api = React.useCallback(function (path, body) {
|
|
403
|
+
return fetch(API_BASE + path, {
|
|
404
|
+
method: body === undefined ? 'GET' : 'POST',
|
|
405
|
+
headers: body === undefined ? undefined : { 'content-type': 'application/json' },
|
|
406
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
407
|
+
}).then(function (response) {
|
|
408
|
+
return response.text().then(function (text) {
|
|
409
|
+
var payload = text.length > 0 ? JSON.parse(text) : {}
|
|
410
|
+
if (!response.ok) throw new Error(payload.error || ('HTTP ' + response.status))
|
|
411
|
+
return payload
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
}, [])
|
|
415
|
+
|
|
416
|
+
var loadStatus = React.useCallback(function (refresh) {
|
|
417
|
+
return api('status' + (refresh ? '?refresh=1' : ''))
|
|
418
|
+
.then(function (payload) { setStatus(payload); setError(null) })
|
|
419
|
+
.catch(function (e) { setError(String((e && e.message) || e)) })
|
|
420
|
+
}, [api])
|
|
421
|
+
|
|
422
|
+
var loadNodes = React.useCallback(function () {
|
|
423
|
+
return api('proxies')
|
|
424
|
+
.then(function (payload) { setNodes(payload.nodes || []); setGroups(payload.groups || []) })
|
|
425
|
+
.catch(function () { /* status carries the error */ })
|
|
426
|
+
}, [api])
|
|
427
|
+
|
|
428
|
+
var loadGuard = React.useCallback(function () {
|
|
429
|
+
return api('guard')
|
|
430
|
+
.then(function (payload) { setGuard(payload) })
|
|
431
|
+
.catch(function () { /* non-fatal */ })
|
|
432
|
+
}, [api])
|
|
433
|
+
|
|
434
|
+
React.useEffect(function () {
|
|
435
|
+
ensureCss()
|
|
436
|
+
loadStatus(false)
|
|
437
|
+
loadNodes()
|
|
438
|
+
loadGuard()
|
|
439
|
+
var statusTimer = setInterval(function () { loadStatus(false); loadGuard() }, 5000)
|
|
440
|
+
var nodeTimer = setInterval(function () { loadNodes() }, 15000)
|
|
441
|
+
return function () { clearInterval(statusTimer); clearInterval(nodeTimer) }
|
|
442
|
+
}, [loadStatus, loadNodes, loadGuard])
|
|
443
|
+
|
|
444
|
+
function run(label, task) {
|
|
445
|
+
setBusy(label)
|
|
446
|
+
setError(null)
|
|
447
|
+
Promise.resolve()
|
|
448
|
+
.then(task)
|
|
449
|
+
.catch(function (e) { setError(String((e && e.message) || e)) })
|
|
450
|
+
.then(function () { setBusy(null) })
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
var stateBadgeClass = 'dshdl-badge'
|
|
454
|
+
if (status) {
|
|
455
|
+
if (status.state === 'running') stateBadgeClass += ' dshdl-ok'
|
|
456
|
+
else if (status.state === 'failed') stateBadgeClass += ' dshdl-err'
|
|
457
|
+
else stateBadgeClass += ' dshdl-warn'
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
var profileList = status && Array.isArray(status.profiles) ? status.profiles : []
|
|
461
|
+
|
|
462
|
+
var upstreamRows = [
|
|
463
|
+
React.createElement(Field, { key: 'proxyUrl', label: 'proxyUrl', value: value.proxyUrl, type: 'text',
|
|
464
|
+
hint: '显式上游,支持 http:// 与 socks5://。填了就不再用订阅。',
|
|
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')))
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
return React.createElement('section', { className: 'dshdl' },
|
|
558
|
+
React.createElement('h2', { className: 'dshdl-title' }, t('title')),
|
|
559
|
+
React.createElement('p', { className: 'dshdl-desc' }, t('intro')),
|
|
560
|
+
|
|
561
|
+
React.createElement('div', { className: 'dshdl-card' },
|
|
562
|
+
React.createElement('div', { className: 'dshdl-cardHead' },
|
|
563
|
+
React.createElement('span', { className: 'dshdl-cardTitle' }, t('upstream')),
|
|
564
|
+
React.createElement('div', { className: 'dshdl-badges' },
|
|
565
|
+
React.createElement('span', { className: stateBadgeClass }, status ? labelOf(STATE_KEYS, status.state, t) : '…'),
|
|
566
|
+
React.createElement('span', { className: 'dshdl-badge' }, status ? labelOf(MODE_KEYS, status.mode, t) : t('mdNone')),
|
|
567
|
+
React.createElement('span', { className: 'dshdl-badge' }, status && status.port > 0 ? ('127.0.0.1:' + status.port) : t('notRunning')),
|
|
568
|
+
React.createElement('span', { className: 'dshdl-badge' }, (t('nodesBadge') || '节点') + ' ' + (status ? status.nodeCount : 0)))),
|
|
569
|
+
status ? React.createElement('p', { className: 'dshdl-desc' }, status.reason) : null,
|
|
570
|
+
status && status.scope ? React.createElement('p', { className: 'dshdl-desc' }, t('scopeIntro')) : null,
|
|
571
|
+
React.createElement('div', { className: 'dshdl-actions' },
|
|
572
|
+
React.createElement('button', { type: 'button', className: 'dshdl-btn', disabled: !!busy,
|
|
573
|
+
onClick: function () { run('refresh', function () { return loadStatus(true).then(loadNodes) }) } }, t('refresh')),
|
|
574
|
+
React.createElement('button', { type: 'button', className: 'dshdl-btn', disabled: !!busy,
|
|
575
|
+
onClick: function () { run('restart', function () { return api('restart', {}).then(loadStatus).then(loadNodes) }) } }, t('restart')),
|
|
576
|
+
React.createElement('button', { type: 'button', className: 'dshdl-btn', disabled: !!busy,
|
|
577
|
+
onClick: function () { run('update-subscription', function () { return api('update-subscription', {}).then(loadStatus).then(loadNodes) }) } }, t('updateSubscription'))),
|
|
578
|
+
React.createElement(Field, { key: 'enabled', label: 'enabled', value: value.enabled, type: 'bool',
|
|
579
|
+
hint: '关闭后下载工具直接失败,不再解析上游。',
|
|
580
|
+
onChange: function (v) { commitBool('enabled', v) } }),
|
|
581
|
+
upstreamRows,
|
|
582
|
+
status && status.lastSubscriptionUpdate
|
|
583
|
+
? React.createElement('p', { className: 'dshdl-desc' }, '订阅更新时间:' + formatTime(status.lastSubscriptionUpdate))
|
|
584
|
+
: null,
|
|
585
|
+
status && status.nativeNodes > 0 && !status.nativeConnector
|
|
586
|
+
? React.createElement('p', { className: 'dshdl-warn' }, '有 ' + status.nativeNodes + ' 个节点需要可选的 Go 原生连接器(hysteria2 / reality),当前构建未打包,这些节点会被跳过。')
|
|
587
|
+
: null),
|
|
588
|
+
|
|
589
|
+
React.createElement('div', { className: 'dshdl-card' },
|
|
590
|
+
React.createElement('div', { className: 'dshdl-cardHead' },
|
|
591
|
+
React.createElement('span', { className: 'dshdl-cardTitle' }, t('profiles') + ' (' + profileList.length + ')')),
|
|
592
|
+
React.createElement('p', { className: 'dshdl-desc' }, t('profilesIntro')),
|
|
593
|
+
profileList.length === 0
|
|
594
|
+
? React.createElement('div', { className: 'dshdl-empty' }, t('noProfiles'))
|
|
595
|
+
: React.createElement('div', { className: 'dshdl-nodes' }, profileList.map(function (profile) {
|
|
596
|
+
var info = profile.subscriptionInfo || null
|
|
597
|
+
var traffic = ''
|
|
598
|
+
if (info && (info.total || info.expire)) {
|
|
599
|
+
var bits = []
|
|
600
|
+
if (info.total) bits.push('已用 ' + formatBytes((info.upload || 0) + (info.download || 0)) + ' / ' + formatBytes(info.total))
|
|
601
|
+
if (info.expire) bits.push('到期 ' + new Date(info.expire * 1000).toLocaleDateString())
|
|
602
|
+
traffic = ' · ' + bits.join(' · ')
|
|
603
|
+
}
|
|
604
|
+
var meta = profile.source === 'file'
|
|
605
|
+
? t('profileFile')
|
|
606
|
+
: (profile.autoUpdate === true ? t('profileAuto') + ' ' + profile.autoUpdateHours + 'h' : '自动更新关闭')
|
|
607
|
+
return React.createElement('div', { key: profile.id, className: 'dshdl-node' + (profile.current ? ' dshdl-active' : '') },
|
|
608
|
+
React.createElement('span', { className: 'dshdl-nodeName', title: profile.label }, profile.label),
|
|
609
|
+
React.createElement('span', { className: 'dshdl-nodeMeta' }, meta + traffic),
|
|
610
|
+
React.createElement('button', {
|
|
611
|
+
type: 'button', className: 'dshdl-btn dshdl-small', disabled: !!busy || profile.current === true,
|
|
612
|
+
onClick: function () {
|
|
613
|
+
run('profile-select', function () { return api('profile-select', { id: profile.id }).then(function () { return loadStatus(false) }).then(loadNodes) })
|
|
614
|
+
}
|
|
615
|
+
}, t('profileSelect')),
|
|
616
|
+
profile.source === 'url'
|
|
617
|
+
? React.createElement('button', {
|
|
618
|
+
type: 'button', className: 'dshdl-btn dshdl-small', disabled: !!busy,
|
|
619
|
+
onClick: function () {
|
|
620
|
+
run('profile-update', function () { return api('profile-update', { id: profile.id }).then(function () { return loadStatus(false) }).then(loadNodes) })
|
|
621
|
+
}
|
|
622
|
+
}, t('profileUpdate'))
|
|
623
|
+
: null,
|
|
624
|
+
React.createElement('button', {
|
|
625
|
+
type: 'button', className: 'dshdl-btn dshdl-small', disabled: !!busy,
|
|
626
|
+
onClick: function () {
|
|
627
|
+
run('profile-remove', function () { return api('profile-remove', { id: profile.id }).then(function () { return loadStatus(false) }).then(loadNodes) })
|
|
628
|
+
}
|
|
629
|
+
}, t('profileRemove')))
|
|
630
|
+
})),
|
|
631
|
+
React.createElement('div', { className: 'dshdl-row' },
|
|
632
|
+
React.createElement('span', { className: 'dshdl-label' }, t('importUrl')),
|
|
633
|
+
React.createElement('div', { className: 'dshdl-importRow' },
|
|
634
|
+
React.createElement('input', {
|
|
635
|
+
className: 'dshdl-input', type: 'text', value: importUrl, spellCheck: false,
|
|
636
|
+
placeholder: t('importUrlPlaceholder'),
|
|
637
|
+
onChange: function (e) { setImportUrl(e.target.value) }
|
|
638
|
+
}),
|
|
639
|
+
React.createElement('input', {
|
|
640
|
+
className: 'dshdl-input', type: 'text', value: importLabel, spellCheck: false,
|
|
641
|
+
placeholder: t('importLabelPlaceholder'),
|
|
642
|
+
onChange: function (e) { setImportLabel(e.target.value) }
|
|
643
|
+
}),
|
|
644
|
+
React.createElement('div', { className: 'dshdl-actions' },
|
|
645
|
+
React.createElement('button', {
|
|
646
|
+
type: 'button', className: 'dshdl-btn', disabled: !!busy || importUrl.trim().length === 0,
|
|
647
|
+
onClick: function () {
|
|
648
|
+
run('profile-add-url', function () {
|
|
649
|
+
return api('profile-add-url', { value: importUrl, label: importLabel }).then(function () {
|
|
650
|
+
setImportUrl('')
|
|
651
|
+
setImportLabel('')
|
|
652
|
+
return loadStatus(false)
|
|
653
|
+
}).then(loadNodes)
|
|
654
|
+
})
|
|
655
|
+
}
|
|
656
|
+
}, t('importBtn')),
|
|
657
|
+
React.createElement('label', { className: 'dshdl-btn dshdl-fileBtn' }, t('importFile'),
|
|
658
|
+
React.createElement('input', {
|
|
659
|
+
type: 'file', accept: '.yaml,.yml,.txt,.conf', style: { display: 'none' },
|
|
660
|
+
onChange: function (e) {
|
|
661
|
+
var file = e.target.files && e.target.files[0]
|
|
662
|
+
e.target.value = ''
|
|
663
|
+
if (!file) return
|
|
664
|
+
var reader = new FileReader()
|
|
665
|
+
reader.onload = function () {
|
|
666
|
+
run('profile-add-file', function () {
|
|
667
|
+
return api('profile-add-file', { name: file.name, content: String(reader.result || '') }).then(function () {
|
|
668
|
+
return loadStatus(false)
|
|
669
|
+
}).then(loadNodes)
|
|
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) })
|
|
696
|
+
})
|
|
697
|
+
}
|
|
698
|
+
}, t('guardApply')),
|
|
699
|
+
React.createElement('button', {
|
|
700
|
+
type: 'button', className: 'dshdl-btn', disabled: !!busy,
|
|
701
|
+
onClick: function () {
|
|
702
|
+
run('guard-restore', function () {
|
|
703
|
+
return api('guard-restore', {}).then(function (payload) { setGuard(payload.state) })
|
|
704
|
+
})
|
|
705
|
+
}
|
|
706
|
+
}, t('guardRestore'))),
|
|
707
|
+
React.createElement('p', { className: 'dshdl-desc' }, t('guardRestart'))),
|
|
708
|
+
|
|
709
|
+
React.createElement('div', { className: 'dshdl-card' },
|
|
710
|
+
React.createElement('div', { className: 'dshdl-cardHead' },
|
|
711
|
+
React.createElement('span', { className: 'dshdl-cardTitle' }, t('nodes') + ' (' + nodes.length + ')'),
|
|
712
|
+
React.createElement('div', { className: 'dshdl-actions' },
|
|
713
|
+
React.createElement('button', { type: 'button', className: 'dshdl-btn dshdl-small', disabled: !!busy,
|
|
714
|
+
onClick: function () { run('delay-all', function () { return api('delay-all', {}).then(loadNodes) }) } }, t('testAll')),
|
|
715
|
+
React.createElement('button', { type: 'button', className: 'dshdl-btn dshdl-small', disabled: !!busy,
|
|
716
|
+
onClick: function () { run('clear-latencies', function () { return api('clear-latencies', {}).then(loadNodes) }) } }, t('clearLatency')))),
|
|
717
|
+
React.createElement('div', { className: 'dshdl-nodes' }, nodeRows),
|
|
718
|
+
status && status.traffic
|
|
719
|
+
? React.createElement('p', { className: 'dshdl-desc' },
|
|
720
|
+
'流量 ↑' + formatBytes(status.traffic.up) + ' ↓' + formatBytes(status.traffic.down))
|
|
721
|
+
: null),
|
|
722
|
+
|
|
723
|
+
React.createElement('div', { className: 'dshdl-card' },
|
|
724
|
+
React.createElement('span', { className: 'dshdl-cardTitle' }, t('rules')),
|
|
725
|
+
rulesRows),
|
|
726
|
+
|
|
727
|
+
React.createElement('div', { className: 'dshdl-card' },
|
|
728
|
+
React.createElement('span', { className: 'dshdl-cardTitle' }, t('download')),
|
|
729
|
+
downloadRows),
|
|
730
|
+
|
|
731
|
+
error ? React.createElement('p', { className: 'dshdl-error' }, error) : null,
|
|
732
|
+
snap && snap.status === 'unavailable' ? React.createElement('p', { className: 'dshdl-warn' }, t('unavailable')) : null,
|
|
733
|
+
|
|
734
|
+
React.createElement('p', { className: 'dshdl-desc' },
|
|
735
|
+
'注意:本插件只管它自己的下载。web_fetch 与 shell 里的 curl / git / npm 由 DSH 启动期的代理策略决定 —— 需要它们也走代理,请在 $DSH_HOME/.env 里设置 HTTP_PROXY / HTTPS_PROXY。'))
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ------------------------------------------------------------------
|
|
739
|
+
// 侧边栏流量徽标
|
|
740
|
+
// ------------------------------------------------------------------
|
|
741
|
+
/**
|
|
742
|
+
* 实时显示经过本插件内核的上下行速率与累计流量。
|
|
743
|
+
*
|
|
744
|
+
* 数据来自 `/traffic`(内核的字节计数器)。这个插槽同时服务展开的侧边栏与
|
|
745
|
+
* 56px 收起栏,`props.wide` 决定用哪种排版;收起态只放得下一个速率。
|
|
746
|
+
* 页面隐藏时降到 5 秒一次,避免后台空转。
|
|
747
|
+
*/
|
|
748
|
+
function TrafficBadge(props) {
|
|
749
|
+
var wide = props.wide !== false
|
|
750
|
+
var t = props.t || function (key) { return key }
|
|
751
|
+
var state = React.useState(null)
|
|
752
|
+
var traffic = state[0]
|
|
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
|
|
780
|
+
var upRate = Number(traffic.upRate) || 0
|
|
781
|
+
var downRate = Number(traffic.downRate) || 0
|
|
782
|
+
var tip = t('trafficTitle') + '\n' +
|
|
783
|
+
t('trafficUp') + ':' + formatRate(upRate) + '(' + t('trafficTotal') + ' ' + formatBytes(up) + ')\n' +
|
|
784
|
+
t('trafficDown') + ':' + formatRate(downRate) + '(' + t('trafficTotal') + ' ' + formatBytes(down) + ')'
|
|
785
|
+
|
|
786
|
+
if (!wide) {
|
|
787
|
+
return React.createElement('div', { className: 'dshdl-trafRail', title: tip },
|
|
788
|
+
React.createElement('span', { className: 'dshdl-trafRailArrow' }, '↓'),
|
|
789
|
+
React.createElement('span', { className: 'dshdl-trafRailRate' }, formatRateTiny(downRate)))
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return React.createElement('div', { className: 'dshdl-traf', title: tip },
|
|
793
|
+
React.createElement('div', { className: 'dshdl-trafRow' },
|
|
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))))
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// ------------------------------------------------------------------
|
|
804
|
+
// 插件主体
|
|
805
|
+
// ------------------------------------------------------------------
|
|
806
|
+
// 只把骨架服务放进 inject;configForms / locale 一律惰性解析,
|
|
807
|
+
// 否则任一服务缺失会让整个客户端插件停在 pending 且不报错。
|
|
808
|
+
var inject = ['slots']
|
|
809
|
+
|
|
810
|
+
function apply(ctx) {
|
|
811
|
+
ensureCss()
|
|
812
|
+
|
|
813
|
+
var translate = null
|
|
814
|
+
try {
|
|
815
|
+
ctx.locale.register(LOCALE_NS, { zh: LOCALE_ZH, en: LOCALE_EN })
|
|
816
|
+
translate = ctx.locale.bind(LOCALE_NS)
|
|
817
|
+
} catch (e) { /* 字典注册失败不影响面板 */ }
|
|
818
|
+
|
|
819
|
+
var cached = null
|
|
820
|
+
function getConfigForm() {
|
|
821
|
+
if (cached) return cached
|
|
822
|
+
try {
|
|
823
|
+
var service = ctx.get('configForms')
|
|
824
|
+
if (service && typeof service.get === 'function') cached = service.get(ENTRY_ID)
|
|
825
|
+
} catch (e) { /* ignore */ }
|
|
826
|
+
return cached
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
ctx.slots.inject('settings.section', function () {
|
|
830
|
+
return ctx.slots.register({
|
|
831
|
+
name: 'settings.section',
|
|
832
|
+
id: ENTRY_ID,
|
|
833
|
+
order: 91,
|
|
834
|
+
label: function () { return translate ? translate('nav') : '下载代理' },
|
|
835
|
+
locale: LOCALE_NS,
|
|
836
|
+
inject: function () {
|
|
837
|
+
return {
|
|
838
|
+
scope: getConfigForm(),
|
|
839
|
+
t: function (key) { return translate ? translate(key) : key }
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}, DownloaderSection)
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
// 侧边栏页脚:实时代理流量。与设置页各自独立,设置页没打开也能看。
|
|
846
|
+
ctx.slots.inject('sidebar.footer.action', function () {
|
|
847
|
+
return ctx.slots.register({
|
|
848
|
+
name: 'sidebar.footer.action',
|
|
849
|
+
id: ENTRY_ID + '-traffic',
|
|
850
|
+
order: 20,
|
|
851
|
+
inject: function () {
|
|
852
|
+
return { t: function (key) { return translate ? translate(key) : key } }
|
|
853
|
+
}
|
|
854
|
+
}, TrafficBadge)
|
|
855
|
+
})
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
exports.apply = apply
|
|
859
|
+
exports.inject = inject
|
|
860
|
+
return module.exports
|
|
861
|
+
}
|
|
862
|
+
})
|