dsh-flash-proxy 0.0.0-stage → 0.1.4
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/LICENSE +201 -0
- package/README.md +50 -3
- package/README.zh-CN.md +86 -0
- package/cordis.patch.yml +4 -0
- package/dist/index.js +714 -0
- package/lib/client.js +999 -0
- package/package.json +59 -4
package/lib/client.js
ADDED
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
// dsh-flash-proxy — CLIENT half of the system proxy control plugin.
|
|
2
|
+
//
|
|
3
|
+
// Registers 5 QuickControl switches via the dock-flash registry:
|
|
4
|
+
// system-proxy, test-url, test-connection, proxy-log, proxy-env
|
|
5
|
+
//
|
|
6
|
+
// Dual-discovery pattern: listens for ‘dock-flash:ready’ event AND
|
|
7
|
+
// checks ‘ctx.get('quickControl')’ for immediate availability.
|
|
8
|
+
|
|
9
|
+
window.__ModuleLoader__ = window.__ModuleLoader__ || {}
|
|
10
|
+
window.__ModuleLoader__.load({
|
|
11
|
+
id: 'dsh-flash-proxy',
|
|
12
|
+
factory: function (require) {
|
|
13
|
+
var module = { exports: {} }
|
|
14
|
+
var exports = module.exports
|
|
15
|
+
|
|
16
|
+
var React = require('react')
|
|
17
|
+
var h = React.createElement
|
|
18
|
+
|
|
19
|
+
exports.name = 'dsh-flash-proxy'
|
|
20
|
+
exports.inject = ['remote', 'remote.settings']
|
|
21
|
+
exports.apply = function (ctx) {
|
|
22
|
+
// A throw anywhere inside the real apply used to propagate straight out
|
|
23
|
+
// of this SYNC apply — and a failed apply FAILS the plugin's Cordis
|
|
24
|
+
// fiber, so the plugin never activates and its switches silently never
|
|
25
|
+
// appear. Registration failures are REPORTED here, never fatal.
|
|
26
|
+
try {
|
|
27
|
+
return _applyInner(ctx)
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error('[dsh-flash-proxy] apply failed (non-fatal):', err)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function _applyInner(ctx) {
|
|
33
|
+
console.log('[dsh-flash-proxy] client apply() — loaded')
|
|
34
|
+
|
|
35
|
+
// Track whether we already registered to avoid double-registration
|
|
36
|
+
// when both ctx.inject(['quickControl']) and dock-flash:ready fire.
|
|
37
|
+
var _registered = false
|
|
38
|
+
|
|
39
|
+
// ── i18n tables ─────────────────────────────────────────
|
|
40
|
+
var zh = {
|
|
41
|
+
systemProxy: '系统代理',
|
|
42
|
+
proxyEnabledOn: '已开启',
|
|
43
|
+
proxyEnabledOff: '已关闭',
|
|
44
|
+
proxyMode: '代理模式',
|
|
45
|
+
noProxyHint: 'NO_PROXY',
|
|
46
|
+
noProxyNotSet: '未设置',
|
|
47
|
+
proxyAllProxy: '全部代理',
|
|
48
|
+
proxyApiBypass: '仅 API 绕过',
|
|
49
|
+
proxyAllBypass: '全部绕过',
|
|
50
|
+
proxyCustom: '自定义',
|
|
51
|
+
proxyModeDescAllProxy: '所有请求都经代理发出',
|
|
52
|
+
proxyModeDescApiBypass: '仅 API/聊天域名直连,其余走代理',
|
|
53
|
+
proxyModeDescAllBypass: '所有请求都直连,不经代理',
|
|
54
|
+
proxyModeDescCustom: '按自定义 NO_PROXY 列表绕过',
|
|
55
|
+
proxyCustomPrompt: '输入自定义 NO_PROXY 值',
|
|
56
|
+
proxyCustomEmpty: '自定义 NO_PROXY 不能为空:要全部走代理请选「全部代理」,要全部绕过请选「全部绕过」或填 *。',
|
|
57
|
+
proxyCustomInvalid: '自定义 NO_PROXY 中有无效条目:',
|
|
58
|
+
proxyCustomSyntax: '只接受主机名、域名后缀、IP 或「主机:端口」,用逗号或空格分隔;域名条目同时匹配其子域(example.com 也匹配 api.example.com);端口需在 1-65535。不支持 CIDR(10.0.0.0/8 请改写成 10.0.0.0),也不要填代理地址本身(http://…)。',
|
|
59
|
+
proxyScopeHint: '仅影响 DSH 进程内的 fetch 请求',
|
|
60
|
+
proxyNoProxyEnv: '未检测到 HTTP_PROXY,代理设置暂无效果',
|
|
61
|
+
proxyEnvRefreshing: '刷新中…',
|
|
62
|
+
testConnection: '测试连接',
|
|
63
|
+
testRunning: '测试中…',
|
|
64
|
+
testSuccess: '连接成功',
|
|
65
|
+
testFailed: '连接失败',
|
|
66
|
+
testUrl: '测试 URL',
|
|
67
|
+
testUrlPrompt: '输入用于测试连接的 URL(http:// 或 https://)',
|
|
68
|
+
testUrlInvalid: 'URL 无效:必须以 http:// 或 https:// 开头',
|
|
69
|
+
proxyLog: '连接诊断日志',
|
|
70
|
+
proxyEnv: '代理环境',
|
|
71
|
+
proxyLogClear: '清空日志',
|
|
72
|
+
logRoute: '链路',
|
|
73
|
+
logProxied: '经代理',
|
|
74
|
+
logDirect: '直连',
|
|
75
|
+
logNoHttpProxy: '未设置 HTTP_PROXY',
|
|
76
|
+
logRedirects: '重定向',
|
|
77
|
+
logRedirectLimit: '已达重定向上限,未继续跟随',
|
|
78
|
+
logFinalUrl: '最终 URL',
|
|
79
|
+
logResponse: '响应',
|
|
80
|
+
logHeaders: '响应头',
|
|
81
|
+
logBody: '主体',
|
|
82
|
+
logTotal: '总计',
|
|
83
|
+
logCause: '原因',
|
|
84
|
+
logFailed: '失败',
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
var en = {
|
|
88
|
+
systemProxy: 'System Proxy',
|
|
89
|
+
proxyEnabledOn: 'Enabled',
|
|
90
|
+
proxyEnabledOff: 'Disabled',
|
|
91
|
+
proxyMode: 'Proxy Mode',
|
|
92
|
+
noProxyHint: 'NO_PROXY',
|
|
93
|
+
noProxyNotSet: 'not set',
|
|
94
|
+
proxyAllProxy: 'All Proxy',
|
|
95
|
+
proxyApiBypass: 'API Bypass',
|
|
96
|
+
proxyAllBypass: 'All Bypass',
|
|
97
|
+
proxyCustom: 'Custom',
|
|
98
|
+
proxyModeDescAllProxy: 'All requests route through the proxy',
|
|
99
|
+
proxyModeDescApiBypass: 'Only API/chat domains bypass; everything else uses the proxy',
|
|
100
|
+
proxyModeDescAllBypass: 'All requests go direct, bypassing the proxy',
|
|
101
|
+
proxyModeDescCustom: 'Bypass per the custom NO_PROXY list',
|
|
102
|
+
proxyCustomPrompt: 'Enter custom NO_PROXY value',
|
|
103
|
+
proxyCustomEmpty: 'Custom NO_PROXY cannot be empty — choose "All Proxy" to bypass nothing, or "All Bypass" / * to bypass everything.',
|
|
104
|
+
proxyCustomInvalid: 'Invalid entry in custom NO_PROXY:',
|
|
105
|
+
proxyCustomSyntax: 'Accepted: host names, domain suffixes, IPs, or host:port, separated by commas or spaces. A domain also matches its subdomains (example.com also matches api.example.com); ports must be 1-65535. CIDR is not supported (rewrite 10.0.0.0/8 as 10.0.0.0), and the proxy address itself (http://…) does not belong here.',
|
|
106
|
+
proxyScopeHint: 'Only affects fetch() requests within DSH process',
|
|
107
|
+
proxyNoProxyEnv: 'No HTTP_PROXY detected — proxy setting has no effect',
|
|
108
|
+
proxyEnvRefreshing: 'Refreshing…',
|
|
109
|
+
testConnection: 'Test Connection',
|
|
110
|
+
testRunning: 'Testing…',
|
|
111
|
+
testSuccess: 'Connected',
|
|
112
|
+
testFailed: 'Failed',
|
|
113
|
+
testUrl: 'Test URL',
|
|
114
|
+
testUrlPrompt: 'URL to test connectivity against (http:// or https://)',
|
|
115
|
+
testUrlInvalid: 'Invalid URL: must start with http:// or https://',
|
|
116
|
+
proxyLog: 'Diagnostics Log',
|
|
117
|
+
proxyEnv: 'Proxy Environment',
|
|
118
|
+
proxyLogClear: 'Clear log',
|
|
119
|
+
logRoute: 'Route',
|
|
120
|
+
logProxied: 'proxied',
|
|
121
|
+
logDirect: 'direct',
|
|
122
|
+
logNoHttpProxy: 'no HTTP_PROXY',
|
|
123
|
+
logRedirects: 'Redirects',
|
|
124
|
+
logRedirectLimit: 'redirect limit reached, stopped following',
|
|
125
|
+
logFinalUrl: 'Final URL',
|
|
126
|
+
logResponse: 'Response',
|
|
127
|
+
logHeaders: 'headers',
|
|
128
|
+
logBody: 'Body',
|
|
129
|
+
logTotal: 'Total',
|
|
130
|
+
logCause: 'Cause',
|
|
131
|
+
logFailed: 'Failed',
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── i18n: reactive locale system ────────────────────────────────
|
|
135
|
+
var LOCALES = { zh: zh, en: en }
|
|
136
|
+
var localeListeners = new Set()
|
|
137
|
+
|
|
138
|
+
function detectLocaleTag() {
|
|
139
|
+
try {
|
|
140
|
+
var tag = document.documentElement.lang
|
|
141
|
+
if (tag) return tag
|
|
142
|
+
} catch (_) {}
|
|
143
|
+
try { return navigator.language || 'en' } catch (_) { return 'en' }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveLocaleKey(tag) {
|
|
147
|
+
var lower = (tag || '').toLowerCase()
|
|
148
|
+
if (lower.startsWith('zh')) return 'zh'
|
|
149
|
+
return 'en'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
var _currentLocaleKey = resolveLocaleKey(detectLocaleTag())
|
|
153
|
+
|
|
154
|
+
if (typeof document !== 'undefined' && typeof MutationObserver !== 'undefined') {
|
|
155
|
+
var _observer = new MutationObserver(function (mutations) {
|
|
156
|
+
for (var i = 0; i < mutations.length; i++) {
|
|
157
|
+
var m = mutations[i]
|
|
158
|
+
if (m.attributeName === 'lang') {
|
|
159
|
+
var newKey = resolveLocaleKey(detectLocaleTag())
|
|
160
|
+
if (newKey !== _currentLocaleKey) {
|
|
161
|
+
_currentLocaleKey = newKey
|
|
162
|
+
localeListeners.forEach(function (fn) { try { fn() } catch (_) {} })
|
|
163
|
+
}
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
_observer.observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function t(k) {
|
|
172
|
+
var dict = LOCALES[_currentLocaleKey] || LOCALES.en
|
|
173
|
+
return dict[k] || LOCALES.en[k] || k
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── localStorage keys ────────────────────────────────────
|
|
177
|
+
var _proxyEnabledKey = 'dsh-flash-proxy:enabled'
|
|
178
|
+
var _proxyModeKey = 'dsh-flash-proxy:proxy-mode'
|
|
179
|
+
var _customNoProxyKey = 'dsh-flash-proxy:custom-no-proxy'
|
|
180
|
+
var _testUrlKey = 'dsh-flash-proxy:test-url'
|
|
181
|
+
|
|
182
|
+
var _PROXY_MODES = ['all-proxy', 'api-bypass', 'all-bypass', 'custom']
|
|
183
|
+
var _DEFAULT_MODE = 'all-proxy'
|
|
184
|
+
|
|
185
|
+
// Global master switch state — ON by default (mirrors dock-flash's
|
|
186
|
+
// System Alerts toggle). When OFF the whole proxy feature is disabled
|
|
187
|
+
// and every outbound request goes direct; the mode selector and other
|
|
188
|
+
// controls are hidden until it is turned back on.
|
|
189
|
+
var _getProxyEnabled = function () {
|
|
190
|
+
try {
|
|
191
|
+
var v = localStorage.getItem(_proxyEnabledKey)
|
|
192
|
+
if (v === '0') return false
|
|
193
|
+
return true
|
|
194
|
+
} catch (_) { return true }
|
|
195
|
+
}
|
|
196
|
+
var _setProxyEnabled = function (v) {
|
|
197
|
+
try { localStorage.setItem(_proxyEnabledKey, v ? '1' : '0') } catch (_) {}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
var _getProxyMode = function () {
|
|
201
|
+
try {
|
|
202
|
+
var v = localStorage.getItem(_proxyModeKey)
|
|
203
|
+
if (v && _PROXY_MODES.indexOf(v) >= 0) return v
|
|
204
|
+
return _DEFAULT_MODE
|
|
205
|
+
} catch (_) { return _DEFAULT_MODE }
|
|
206
|
+
}
|
|
207
|
+
var _setProxyMode = function (v) {
|
|
208
|
+
try { localStorage.setItem(_proxyModeKey, String(v)) } catch (_) {}
|
|
209
|
+
}
|
|
210
|
+
var _getCustomNoProxy = function () {
|
|
211
|
+
try { return localStorage.getItem(_customNoProxyKey) || '' } catch (_) { return '' }
|
|
212
|
+
}
|
|
213
|
+
var _setCustomNoProxy = function (v) {
|
|
214
|
+
try { localStorage.setItem(_customNoProxyKey, String(v)) } catch (_) {}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Custom NO_PROXY validation ──────────────────────────
|
|
218
|
+
var _NO_PROXY_HOST_RE = /^[a-z0-9]([a-z0-9_-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9_-]*[a-z0-9])?)*\.?$/i
|
|
219
|
+
var _NO_PROXY_IPV4_RE = /^\d{1,3}(\.\d{1,3}){3}$/
|
|
220
|
+
var _NO_PROXY_IPV6_RE = /^[0-9a-f:]+$/i
|
|
221
|
+
|
|
222
|
+
function _normalizeNoProxyList(raw) {
|
|
223
|
+
var entries = String(raw == null ? '' : raw).split(/[,\s]+/).filter(function (e) { return e !== '' })
|
|
224
|
+
if (entries.length === 0) return { ok: false, entry: '', reason: 'empty' }
|
|
225
|
+
var out = []
|
|
226
|
+
for (var i = 0; i < entries.length; i++) {
|
|
227
|
+
var entry = entries[i]
|
|
228
|
+
if (entry === '*') { out.push('*'); continue }
|
|
229
|
+
if (/[/?#@\\]/.test(entry)) return { ok: false, entry: entry, reason: 'chars' }
|
|
230
|
+
var body = entry.replace(/^\*\./, '').replace(/^\./, '')
|
|
231
|
+
var host = body
|
|
232
|
+
var port = null
|
|
233
|
+
if (body.charAt(0) === '[') {
|
|
234
|
+
var bracketed = /^\[([^\]]+)\](?::(\d+))?$/.exec(body)
|
|
235
|
+
if (!bracketed) return { ok: false, entry: entry, reason: 'host' }
|
|
236
|
+
host = bracketed[1]
|
|
237
|
+
port = bracketed[2] === undefined ? null : bracketed[2]
|
|
238
|
+
} else {
|
|
239
|
+
var first = body.indexOf(':')
|
|
240
|
+
if (first !== -1 && first === body.lastIndexOf(':')) {
|
|
241
|
+
host = body.slice(0, first)
|
|
242
|
+
port = body.slice(first + 1)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (port !== null) {
|
|
246
|
+
var portNum = /^\d+$/.test(port) ? Number(port) : NaN
|
|
247
|
+
if (!(portNum >= 1 && portNum <= 65535)) return { ok: false, entry: entry, reason: 'port' }
|
|
248
|
+
}
|
|
249
|
+
if (host === '') return { ok: false, entry: entry, reason: 'host' }
|
|
250
|
+
if (host.indexOf(':') !== -1) {
|
|
251
|
+
if (!_NO_PROXY_IPV6_RE.test(host)) return { ok: false, entry: entry, reason: 'host' }
|
|
252
|
+
} else if (_NO_PROXY_IPV4_RE.test(host)) {
|
|
253
|
+
var octets = host.split('.')
|
|
254
|
+
for (var o = 0; o < octets.length; o++) {
|
|
255
|
+
if (Number(octets[o]) > 255) return { ok: false, entry: entry, reason: 'host' }
|
|
256
|
+
}
|
|
257
|
+
} else if (!_NO_PROXY_HOST_RE.test(host)) {
|
|
258
|
+
return { ok: false, entry: entry, reason: 'host' }
|
|
259
|
+
}
|
|
260
|
+
out.push(entry)
|
|
261
|
+
}
|
|
262
|
+
return { ok: true, value: out.join(',') }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function _noProxyErrorText(result) {
|
|
266
|
+
if (result.reason === 'empty') return t('proxyCustomEmpty')
|
|
267
|
+
return t('proxyCustomInvalid') + ' "' + result.entry + '"\n' + t('proxyCustomSyntax')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Test target presets ────────────────────────────────
|
|
271
|
+
var TEST_URL_PRESETS = [
|
|
272
|
+
{ value: 'google', url: 'https://www.google.com/generate_204', label: 'Google 204' },
|
|
273
|
+
{ value: 'github', url: 'https://github.com', label: 'GitHub' },
|
|
274
|
+
{ value: 'deepseek', url: 'https://api.deepseek.com', label: 'DeepSeek API' },
|
|
275
|
+
]
|
|
276
|
+
var _getTestUrl = function () {
|
|
277
|
+
try { return localStorage.getItem(_testUrlKey) || '' } catch (_) { return '' }
|
|
278
|
+
}
|
|
279
|
+
var _setTestUrl = function (v) {
|
|
280
|
+
try {
|
|
281
|
+
if (v) localStorage.setItem(_testUrlKey, String(v))
|
|
282
|
+
else localStorage.removeItem(_testUrlKey)
|
|
283
|
+
} catch (_) {}
|
|
284
|
+
}
|
|
285
|
+
var _resolveTestUrl = function () { return _getTestUrl() || TEST_URL_PRESETS[0].url }
|
|
286
|
+
var _testUrlPresetValue = function (url) {
|
|
287
|
+
for (var i = 0; i < TEST_URL_PRESETS.length; i++) {
|
|
288
|
+
if (TEST_URL_PRESETS[i].url === url) return TEST_URL_PRESETS[i].value
|
|
289
|
+
}
|
|
290
|
+
return url ? 'custom' : TEST_URL_PRESETS[0].value
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── Diagnostics log formatting ───────────────────────────
|
|
294
|
+
var _proxyLog = []
|
|
295
|
+
var _proxyLogMeta = ''
|
|
296
|
+
function _setLog(lines) {
|
|
297
|
+
_proxyLog = lines.slice()
|
|
298
|
+
}
|
|
299
|
+
function _logStamp() {
|
|
300
|
+
var d = new Date()
|
|
301
|
+
var p = function (n) { return (n < 10 ? '0' : '') + n }
|
|
302
|
+
return p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())
|
|
303
|
+
}
|
|
304
|
+
function _fmtBytes(n) {
|
|
305
|
+
if (typeof n !== 'number') return '?'
|
|
306
|
+
if (n < 1024) return n + ' B'
|
|
307
|
+
if (n < 1048576) return (n / 1024).toFixed(1) + ' KB'
|
|
308
|
+
return (n / 1048576).toFixed(2) + ' MB'
|
|
309
|
+
}
|
|
310
|
+
function _oneLine(v, max) {
|
|
311
|
+
var s = (v === null || v === undefined) ? '' : String(v)
|
|
312
|
+
s = s.replace(/\s+/g, ' ').trim()
|
|
313
|
+
return (max && s.length > max) ? s.slice(0, max) + '…' : s
|
|
314
|
+
}
|
|
315
|
+
function _describeTest(data) {
|
|
316
|
+
var out = []
|
|
317
|
+
var proxy = (data && data.proxy) || {}
|
|
318
|
+
out.push('[' + _logStamp() + '] ▶ ' + _oneLine((data && data.url) || '?', 300))
|
|
319
|
+
var bits = [proxy.proxied ? t('logProxied') : t('logDirect')]
|
|
320
|
+
bits.push(proxy.httpProxy
|
|
321
|
+
? 'HTTP_PROXY=' + _oneLine(proxy.httpProxy, 120)
|
|
322
|
+
: t('logNoHttpProxy'))
|
|
323
|
+
bits.push(t('noProxyHint') + '=' + _oneLine(proxy.noProxy || '(' + t('noProxyNotSet') + ')', 200))
|
|
324
|
+
if (proxy.mode) bits.push(t('systemProxy') + '=' + _oneLine(proxy.mode, 40))
|
|
325
|
+
out.push(' ' + t('logRoute') + ': ' + bits.join(' · '))
|
|
326
|
+
if (proxy.routeError) {
|
|
327
|
+
out.push(' ' + t('logCause') + ': proxyRouteFor — ' + _oneLine(proxy.routeError, 300))
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
var hops = (data && data.redirects) || []
|
|
331
|
+
if (hops.length) {
|
|
332
|
+
out.push(' ' + t('logRedirects') + ': ' + hops.length)
|
|
333
|
+
for (var i = 0; i < hops.length; i++) {
|
|
334
|
+
out.push(' ↳ ' + hops[i].hop + ') ' + hops[i].status + ' → ' + _oneLine(hops[i].to, 300))
|
|
335
|
+
}
|
|
336
|
+
if (data.redirectLimitHit) out.push(' ↳ ' + t('logRedirectLimit'))
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (data && data.ok) {
|
|
340
|
+
out.push(' ' + t('logResponse') + ': ' + data.status +
|
|
341
|
+
(data.statusText ? ' ' + _oneLine(data.statusText, 60) : '') +
|
|
342
|
+
' · ' + t('logHeaders') + ' ' + data.headersMs + 'ms')
|
|
343
|
+
if (data.contentType) out.push(' content-type: ' + _oneLine(data.contentType, 120))
|
|
344
|
+
if (data.contentLength) out.push(' content-length: ' + _oneLine(data.contentLength, 40))
|
|
345
|
+
out.push(' ' + t('logBody') + ': ' + _fmtBytes(data.bodyBytes) + ' · ' + data.bodyMs + 'ms')
|
|
346
|
+
if (data.bodySnippet) {
|
|
347
|
+
out.push(' ⤴ ' + _oneLine(data.bodySnippet, 200))
|
|
348
|
+
}
|
|
349
|
+
out.push(' ' + t('logTotal') + ': ' + data.elapsedMs + 'ms · ' + t('testSuccess'))
|
|
350
|
+
} else {
|
|
351
|
+
var err = (data && data.error) || {}
|
|
352
|
+
out.push(' ' + t('logFailed') + ': ' + _oneLine(err.name || 'Error', 60) +
|
|
353
|
+
' — ' + _oneLine(err.message || 'unknown', 300))
|
|
354
|
+
var cause = [err.causeCode, err.causeMessage].filter(Boolean).join(' — ')
|
|
355
|
+
if (cause) out.push(' ' + t('logCause') + ': ' + _oneLine(cause, 300))
|
|
356
|
+
else if (err.causeName) out.push(' ' + t('logCause') + ': ' + _oneLine(err.causeName, 120))
|
|
357
|
+
if (err.code) out.push(' code: ' + _oneLine(err.code, 80))
|
|
358
|
+
if (data && data.finalUrl && data.finalUrl !== data.url) {
|
|
359
|
+
out.push(' ' + t('logFinalUrl') + ': ' + _oneLine(data.finalUrl, 300))
|
|
360
|
+
}
|
|
361
|
+
out.push(' ' + t('logTotal') + ': ' + ((data && data.elapsedMs) || 0) + 'ms · ' + t('testFailed'))
|
|
362
|
+
}
|
|
363
|
+
return out
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Proxy status tracking ──────────────────────────────
|
|
367
|
+
var _noProxyValue = null
|
|
368
|
+
var _hostProxyMode = null
|
|
369
|
+
var _proxyAvailable = null
|
|
370
|
+
var _httpProxyValue = null
|
|
371
|
+
var _proxyEnvValue = null
|
|
372
|
+
var _proxyEnvStale = false
|
|
373
|
+
var _suppressHostSync = false
|
|
374
|
+
var _hostProxyEnabled = null
|
|
375
|
+
|
|
376
|
+
function _fetchProxyStatus() {
|
|
377
|
+
try {
|
|
378
|
+
fetch('/plugins/dsh-flash-proxy/proxy-status')
|
|
379
|
+
.then(function (r) { return r.json() })
|
|
380
|
+
.then(function (data) {
|
|
381
|
+
if (data) {
|
|
382
|
+
var changed = false
|
|
383
|
+
// Sync the global master switch from the host. Compare
|
|
384
|
+
// against the host-reported value (not local, which could be
|
|
385
|
+
// stale after a host-side edit) and avoid an echo loop while a
|
|
386
|
+
// local toggle write is in flight.
|
|
387
|
+
if (typeof data.proxyEnabled === 'boolean' && data.proxyEnabled !== _hostProxyEnabled) {
|
|
388
|
+
_hostProxyEnabled = data.proxyEnabled
|
|
389
|
+
changed = true
|
|
390
|
+
}
|
|
391
|
+
if (typeof data.proxyEnabled === 'boolean' &&
|
|
392
|
+
data.proxyEnabled !== _getProxyEnabled() && !_suppressHostSync) {
|
|
393
|
+
_setProxyEnabled(data.proxyEnabled)
|
|
394
|
+
changed = true
|
|
395
|
+
}
|
|
396
|
+
if (typeof data.noProxy !== 'undefined' && data.noProxy !== _noProxyValue) {
|
|
397
|
+
_noProxyValue = data.noProxy
|
|
398
|
+
changed = true
|
|
399
|
+
}
|
|
400
|
+
if (data.proxyMode && data.proxyMode !== _hostProxyMode) {
|
|
401
|
+
_hostProxyMode = data.proxyMode
|
|
402
|
+
changed = true
|
|
403
|
+
}
|
|
404
|
+
if (data.proxyMode && data.proxyMode !== _getProxyMode() && !_suppressHostSync) {
|
|
405
|
+
_setProxyMode(data.proxyMode)
|
|
406
|
+
changed = true
|
|
407
|
+
}
|
|
408
|
+
if (typeof data.proxyAvailable === 'boolean' && data.proxyAvailable !== _proxyAvailable) {
|
|
409
|
+
_proxyAvailable = data.proxyAvailable
|
|
410
|
+
changed = true
|
|
411
|
+
}
|
|
412
|
+
if (data.httpProxy !== undefined && data.httpProxy !== _httpProxyValue) {
|
|
413
|
+
_httpProxyValue = data.httpProxy
|
|
414
|
+
changed = true
|
|
415
|
+
}
|
|
416
|
+
if (data.proxyEnv && (
|
|
417
|
+
data.proxyEnv.http !== (_proxyEnvValue && _proxyEnvValue.http) ||
|
|
418
|
+
data.proxyEnv.https !== (_proxyEnvValue && _proxyEnvValue.https) ||
|
|
419
|
+
data.proxyEnv.all !== (_proxyEnvValue && _proxyEnvValue.all)
|
|
420
|
+
)) {
|
|
421
|
+
_proxyEnvValue = data.proxyEnv
|
|
422
|
+
changed = true
|
|
423
|
+
}
|
|
424
|
+
if (data.proxyEnv) {
|
|
425
|
+
if (_proxyEnvStale) changed = true
|
|
426
|
+
_proxyEnvStale = false
|
|
427
|
+
}
|
|
428
|
+
if (data.customNoProxy !== undefined && data.customNoProxy !== _getCustomNoProxy()) {
|
|
429
|
+
_setCustomNoProxy(data.customNoProxy)
|
|
430
|
+
changed = true
|
|
431
|
+
}
|
|
432
|
+
var testUrlChanged = false
|
|
433
|
+
if (typeof data.testUrl === 'string' && data.testUrl &&
|
|
434
|
+
data.testUrl !== _getTestUrl() && !_suppressHostSync) {
|
|
435
|
+
_setTestUrl(data.testUrl)
|
|
436
|
+
changed = true
|
|
437
|
+
testUrlChanged = true
|
|
438
|
+
}
|
|
439
|
+
if (changed) {
|
|
440
|
+
registry.notifyChange('dsh-flash-proxy:system-proxy')
|
|
441
|
+
// Re-render every switch whose visible() is gated on the
|
|
442
|
+
// global master toggle, so they appear/disappear as soon as
|
|
443
|
+
// the host state flips rather than waiting for the next
|
|
444
|
+
// unrelated render.
|
|
445
|
+
;['proxy-mode', 'test-url', 'test-connection', 'proxy-log', 'proxy-env']
|
|
446
|
+
.forEach(function (sid) {
|
|
447
|
+
try { registry.notifyChange('dsh-flash-proxy:' + sid) } catch (_) {}
|
|
448
|
+
})
|
|
449
|
+
if (testUrlChanged) registry.notifyChange('dsh-flash-proxy:test-url')
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
})
|
|
453
|
+
.catch(function () {})
|
|
454
|
+
} catch (_) {}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ── Settings write queue ──────────────────────────────
|
|
458
|
+
var _hostRevision = null
|
|
459
|
+
var _prefWriteTail = Promise.resolve()
|
|
460
|
+
var _pendingRevision = null
|
|
461
|
+
|
|
462
|
+
function _fenceRevision() {
|
|
463
|
+
if (_pendingRevision !== null) return _pendingRevision
|
|
464
|
+
return _hostRevision === null ? undefined : _hostRevision
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function _remoteSettings(ctx) {
|
|
468
|
+
try {
|
|
469
|
+
if (ctx && ctx.remote && ctx.remote.settings) return ctx.remote.settings
|
|
470
|
+
} catch (_) {}
|
|
471
|
+
try {
|
|
472
|
+
var remote = ctx && ctx.get ? ctx.get('remote') : undefined
|
|
473
|
+
return remote && remote.settings ? remote.settings : undefined
|
|
474
|
+
} catch (_) { return undefined }
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Normalize a `describe()` result into a promise. NEVER let this throw.
|
|
478
|
+
*
|
|
479
|
+
* Calling `.then` directly on a `describe()` result is not safe: the SAME
|
|
480
|
+
* method name has two implementations. The typert WIRE form returns a
|
|
481
|
+
* Promise, but the DIRECT form — `SettingsController.describe()` in
|
|
482
|
+
* `@deepseek-ai/dsh-api-settings-controller` — is SYNCHRONOUS and returns the
|
|
483
|
+
* view object itself; and it can THROW instead ("settings service is absent:
|
|
484
|
+
* mount @deepseek-ai/dsh-settings …"). So `.then` may not exist at all, and
|
|
485
|
+
* the TypeError is thrown SYNCHRONOUSLY, ahead of any `.catch`.
|
|
486
|
+
*
|
|
487
|
+
* WHY THAT IS NOT JUST A LOG LINE: `apply()` here is `async` with no internal
|
|
488
|
+
* try/catch, so the throw becomes a REJECTED apply — the Cordis fiber goes
|
|
489
|
+
* FAILED and the plugin never activates. Its switches simply never appear:
|
|
490
|
+
* the "the plugin was registered and then silently disappeared" symptom,
|
|
491
|
+
* with `TypeError: settings.describe(...).then is not a function` as the
|
|
492
|
+
* only clue. Verified in dock-flash's check:overlay section 23. */
|
|
493
|
+
function _describeAsync(settings) {
|
|
494
|
+
try { return Promise.resolve(settings.describe()) }
|
|
495
|
+
catch (err) { return Promise.reject(err) }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function _queuePrefWrite(ctx, patch, onError, saved) {
|
|
499
|
+
var task = _prefWriteTail.then(function () {
|
|
500
|
+
var settings = _remoteSettings(ctx)
|
|
501
|
+
if (!settings || typeof settings.update !== 'function') {
|
|
502
|
+
if (onError) onError(patch, function () {})
|
|
503
|
+
return false
|
|
504
|
+
}
|
|
505
|
+
return settings.update('dsh-flash-proxy', patch, _fenceRevision())
|
|
506
|
+
.then(function (res) {
|
|
507
|
+
try {
|
|
508
|
+
if (res && res.ok === false) {
|
|
509
|
+
_pendingRevision = null
|
|
510
|
+
console.warn('[dsh-flash-proxy] host rejected the preference write:',
|
|
511
|
+
(res.error && (res.error.message || res.error.code)) || res.error)
|
|
512
|
+
if (onError) onError(patch, function () {})
|
|
513
|
+
try {
|
|
514
|
+
_describeAsync(settings).then(function (desc) {
|
|
515
|
+
if (desc && desc.ok !== false) {
|
|
516
|
+
var view2 = desc.value || desc
|
|
517
|
+
var list2 = view2 && Array.isArray(view2.namespaces)
|
|
518
|
+
? view2.namespaces
|
|
519
|
+
: (Array.isArray(view2) ? view2 : null)
|
|
520
|
+
var ns2 = list2 && list2.find(function (n) {
|
|
521
|
+
return (n && (n.ns || n.namespace)) === 'dsh-flash-proxy'
|
|
522
|
+
})
|
|
523
|
+
if (ns2 && typeof ns2.revision === 'number') _hostRevision = ns2.revision
|
|
524
|
+
}
|
|
525
|
+
}).catch(function () {})
|
|
526
|
+
} catch (_) {}
|
|
527
|
+
return false
|
|
528
|
+
}
|
|
529
|
+
var v = res && res.value
|
|
530
|
+
if (v && typeof v.revision === 'number') {
|
|
531
|
+
_pendingRevision = v.revision
|
|
532
|
+
_hostRevision = v.revision
|
|
533
|
+
}
|
|
534
|
+
} catch (_) {}
|
|
535
|
+
return true
|
|
536
|
+
}).catch(function (err) {
|
|
537
|
+
_pendingRevision = null
|
|
538
|
+
console.warn('[dsh-flash-proxy] failed to persist preference to host:', err)
|
|
539
|
+
if (onError) onError(patch, function () {})
|
|
540
|
+
return false
|
|
541
|
+
})
|
|
542
|
+
})
|
|
543
|
+
_prefWriteTail = task.then(function () {}, function () {})
|
|
544
|
+
return task
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ── Dual-discovery registration ──────────────────────────
|
|
548
|
+
function registerProxySwitches(registry) {
|
|
549
|
+
// Startup host sync
|
|
550
|
+
try {
|
|
551
|
+
var settingsSvc = _remoteSettings(ctx)
|
|
552
|
+
if (settingsSvc && typeof settingsSvc.describe === 'function') {
|
|
553
|
+
_describeAsync(settingsSvc).then(function (desc) {
|
|
554
|
+
if (!desc || desc.ok === false) return
|
|
555
|
+
var view = desc.value || desc
|
|
556
|
+
var nsList = view && Array.isArray(view.namespaces)
|
|
557
|
+
? view.namespaces
|
|
558
|
+
: (Array.isArray(view) ? view : [])
|
|
559
|
+
if (!nsList.length) return
|
|
560
|
+
var ns = nsList.find(function (n) {
|
|
561
|
+
return (n && (n.ns || n.namespace)) === 'dsh-flash-proxy'
|
|
562
|
+
})
|
|
563
|
+
var resolvedNs = ns && (ns.value || ns.resolved)
|
|
564
|
+
if (resolvedNs) {
|
|
565
|
+
var res = resolvedNs
|
|
566
|
+
// Sync the global master switch from the host on startup.
|
|
567
|
+
if (typeof res.proxyEnabled === 'boolean' && res.proxyEnabled !== _getProxyEnabled()) {
|
|
568
|
+
_setProxyEnabled(res.proxyEnabled)
|
|
569
|
+
registry.notifyChange('dsh-flash-proxy:system-proxy')
|
|
570
|
+
console.log('[dsh-flash-proxy] proxyEnabled sync from host: ' + res.proxyEnabled)
|
|
571
|
+
}
|
|
572
|
+
if (res.proxyMode) {
|
|
573
|
+
var hostMode = res.proxyMode
|
|
574
|
+
var localMode = _getProxyMode()
|
|
575
|
+
if (hostMode !== localMode) {
|
|
576
|
+
_setProxyMode(hostMode)
|
|
577
|
+
registry.notifyChange('dsh-flash-proxy:system-proxy')
|
|
578
|
+
console.log('[dsh-flash-proxy] proxy-mode sync: host=' + hostMode + ', local was=' + localMode + ' → synced')
|
|
579
|
+
}
|
|
580
|
+
if (res.customNoProxy !== undefined && res.customNoProxy !== _getCustomNoProxy()) {
|
|
581
|
+
_setCustomNoProxy(res.customNoProxy)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (typeof res.testUrl === 'string' && res.testUrl && res.testUrl !== _getTestUrl()) {
|
|
585
|
+
_setTestUrl(res.testUrl)
|
|
586
|
+
registry.notifyChange('dsh-flash-proxy:test-url')
|
|
587
|
+
console.log('[dsh-flash-proxy] test-url sync: ' + res.testUrl)
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}).catch(function () {})
|
|
591
|
+
}
|
|
592
|
+
} catch (_) {}
|
|
593
|
+
_fetchProxyStatus()
|
|
594
|
+
|
|
595
|
+
// ── Switch 1: system-proxy — global master toggle ──────────
|
|
596
|
+
// Modelled on dock-flash's System Alerts switch: a single
|
|
597
|
+
// on/off that gates the whole feature. When OFF the host treats
|
|
598
|
+
// every request as direct (all-bypass) and the mode selector
|
|
599
|
+
// below — plus test / log / env controls — are hidden until it is
|
|
600
|
+
// turned back on.
|
|
601
|
+
ctx.effect(function () {
|
|
602
|
+
var dispose = registry.registerSwitch({
|
|
603
|
+
id: 'dsh-flash-proxy:system-proxy',
|
|
604
|
+
label: function () { return t('systemProxy') },
|
|
605
|
+
tooltip: function () {
|
|
606
|
+
if (!_getProxyEnabled()) return t('proxyEnabledOff')
|
|
607
|
+
if (!_httpProxyValue) return t('proxyNoProxyEnv')
|
|
608
|
+
return t('proxyScopeHint')
|
|
609
|
+
},
|
|
610
|
+
icon: 'link',
|
|
611
|
+
type: 'toggle',
|
|
612
|
+
cluster: 'system-proxy',
|
|
613
|
+
group: 'system',
|
|
614
|
+
order: 100,
|
|
615
|
+
getValue: _getProxyEnabled,
|
|
616
|
+
setValue: function (v) {
|
|
617
|
+
_setProxyEnabled(v)
|
|
618
|
+
console.log('[dsh-flash-proxy] system proxy ' + (v ? 'enabled' : 'disabled') + ' (global switch)')
|
|
619
|
+
_proxyEnvStale = true
|
|
620
|
+
registry.notifyChange('dsh-flash-proxy:proxy-env')
|
|
621
|
+
_suppressHostSync = true
|
|
622
|
+
try {
|
|
623
|
+
var proxySettings = _remoteSettings(ctx)
|
|
624
|
+
if (proxySettings && typeof proxySettings.update === 'function') {
|
|
625
|
+
_queuePrefWrite(ctx, { proxyEnabled: v }, null, {}).then(function () {
|
|
626
|
+
_suppressHostSync = false
|
|
627
|
+
_fetchProxyStatus()
|
|
628
|
+
}).catch(function (err) {
|
|
629
|
+
console.warn('[dsh-flash-proxy] failed to sync proxyEnabled to host:', err)
|
|
630
|
+
_suppressHostSync = false
|
|
631
|
+
})
|
|
632
|
+
} else {
|
|
633
|
+
console.warn('[dsh-flash-proxy] remote settings unavailable, proxyEnabled change may not persist to host')
|
|
634
|
+
_suppressHostSync = false
|
|
635
|
+
}
|
|
636
|
+
} catch (_) { _suppressHostSync = false }
|
|
637
|
+
// Re-render every switch gated on the master so they
|
|
638
|
+
// appear/disappear immediately.
|
|
639
|
+
;['proxy-mode', 'test-url', 'test-connection', 'proxy-log', 'proxy-env']
|
|
640
|
+
.forEach(function (sid) {
|
|
641
|
+
try { registry.notifyChange('dsh-flash-proxy:' + sid) } catch (_) {}
|
|
642
|
+
})
|
|
643
|
+
},
|
|
644
|
+
})
|
|
645
|
+
return dispose
|
|
646
|
+
}, 'dsh-flash-proxy: system-proxy switch')
|
|
647
|
+
|
|
648
|
+
// ── Switch 2: proxy-mode — NO_PROXY selector (moved inside) ──
|
|
649
|
+
// The proxy-mode dropdown, now nested under the global master
|
|
650
|
+
// toggle: only visible while the system-proxy switch is ON.
|
|
651
|
+
ctx.effect(function () {
|
|
652
|
+
var dispose = registry.registerSwitch({
|
|
653
|
+
id: 'dsh-flash-proxy:proxy-mode',
|
|
654
|
+
label: function () { return t('proxyMode') },
|
|
655
|
+
subtitle: function () {
|
|
656
|
+
var mode = _getProxyMode() || ''
|
|
657
|
+
var desc = t('proxyModeDescAllProxy')
|
|
658
|
+
if (mode === 'api-bypass') desc = t('proxyModeDescApiBypass')
|
|
659
|
+
else if (mode === 'all-bypass') desc = t('proxyModeDescAllBypass')
|
|
660
|
+
else if (mode === 'custom') desc = t('proxyModeDescCustom')
|
|
661
|
+
return desc
|
|
662
|
+
},
|
|
663
|
+
subtitleBlock: true,
|
|
664
|
+
icon: 'globe',
|
|
665
|
+
type: 'select',
|
|
666
|
+
cluster: 'system-proxy',
|
|
667
|
+
group: 'system',
|
|
668
|
+
order: 101,
|
|
669
|
+
visible: function () { return _getProxyEnabled() },
|
|
670
|
+
options: [
|
|
671
|
+
{ value: 'all-proxy', label: function () { return t('proxyAllProxy') } },
|
|
672
|
+
{ value: 'api-bypass', label: function () { return t('proxyApiBypass') } },
|
|
673
|
+
{ value: 'all-bypass', label: function () { return t('proxyAllBypass') } },
|
|
674
|
+
{ value: 'custom', label: function () { return t('proxyCustom') + (_getCustomNoProxy() ? ': ' + _getCustomNoProxy() : '') } },
|
|
675
|
+
],
|
|
676
|
+
getValue: _getProxyMode,
|
|
677
|
+
setValue: function (mode) {
|
|
678
|
+
if (mode === 'custom') {
|
|
679
|
+
var current = _getCustomNoProxy()
|
|
680
|
+
var entered = prompt(t('proxyCustomPrompt'), current)
|
|
681
|
+
if (entered === null) {
|
|
682
|
+
registry.notifyChange('dsh-flash-proxy:proxy-mode')
|
|
683
|
+
return
|
|
684
|
+
}
|
|
685
|
+
var checked = _normalizeNoProxyList(entered)
|
|
686
|
+
if (!checked.ok) {
|
|
687
|
+
alert(_noProxyErrorText(checked))
|
|
688
|
+
registry.notifyChange('dsh-flash-proxy:proxy-mode')
|
|
689
|
+
return
|
|
690
|
+
}
|
|
691
|
+
var typed = String(entered).trim()
|
|
692
|
+
if (checked.value !== typed) {
|
|
693
|
+
console.log('[dsh-flash-proxy] custom NO_PROXY normalised: "' + typed + '" → "' + checked.value + '"')
|
|
694
|
+
}
|
|
695
|
+
_setCustomNoProxy(checked.value)
|
|
696
|
+
}
|
|
697
|
+
_setProxyMode(mode)
|
|
698
|
+
console.log('[dsh-flash-proxy] proxy mode changed: ' + mode + (_getCustomNoProxy() ? ' (NO_PROXY=' + _getCustomNoProxy() + ')' : ''))
|
|
699
|
+
_proxyEnvStale = true
|
|
700
|
+
registry.notifyChange('dsh-flash-proxy:proxy-env')
|
|
701
|
+
_suppressHostSync = true
|
|
702
|
+
try {
|
|
703
|
+
var proxySettings = _remoteSettings(ctx)
|
|
704
|
+
if (proxySettings && typeof proxySettings.update === 'function') {
|
|
705
|
+
_queuePrefWrite(ctx, {
|
|
706
|
+
proxyMode: mode,
|
|
707
|
+
customNoProxy: _getCustomNoProxy(),
|
|
708
|
+
}, null, {}).then(function () {
|
|
709
|
+
_suppressHostSync = false
|
|
710
|
+
_fetchProxyStatus()
|
|
711
|
+
}).catch(function (err) {
|
|
712
|
+
console.warn('[dsh-flash-proxy] failed to sync proxy setting to host:', err)
|
|
713
|
+
_suppressHostSync = false
|
|
714
|
+
registry.recordChange({
|
|
715
|
+
id: 'dsh-flash-proxy:system-proxy',
|
|
716
|
+
label: t('systemProxy'),
|
|
717
|
+
icon: '⚠️',
|
|
718
|
+
oldDisplay: '',
|
|
719
|
+
newDisplay: mode + ' ⚠',
|
|
720
|
+
})
|
|
721
|
+
})
|
|
722
|
+
} else {
|
|
723
|
+
console.warn('[dsh-flash-proxy] remote settings unavailable, proxy change may not persist to host')
|
|
724
|
+
_suppressHostSync = false
|
|
725
|
+
}
|
|
726
|
+
} catch (_) { _suppressHostSync = false }
|
|
727
|
+
},
|
|
728
|
+
})
|
|
729
|
+
return dispose
|
|
730
|
+
}, 'dsh-flash-proxy: proxy-mode switch')
|
|
731
|
+
|
|
732
|
+
// ── settings/updated listener ─────────────────────
|
|
733
|
+
ctx.effect(function () {
|
|
734
|
+
var off = ctx.on('settings/updated', function (ns, next) {
|
|
735
|
+
if (ns !== 'dsh-flash-proxy') return
|
|
736
|
+
if (next) {
|
|
737
|
+
var newEnabled = next.proxyEnabled
|
|
738
|
+
var newMode = next.proxyMode
|
|
739
|
+
var newCustom = next.customNoProxy
|
|
740
|
+
if (typeof newEnabled === 'boolean' && newEnabled !== _getProxyEnabled()) {
|
|
741
|
+
_setProxyEnabled(newEnabled)
|
|
742
|
+
registry.notifyChange('dsh-flash-proxy:system-proxy')
|
|
743
|
+
console.log('[dsh-flash-proxy] proxyEnabled sync from settings/updated: ' + newEnabled)
|
|
744
|
+
}
|
|
745
|
+
if (newMode && newMode !== _getProxyMode()) {
|
|
746
|
+
_setProxyMode(newMode)
|
|
747
|
+
registry.notifyChange('dsh-flash-proxy:system-proxy')
|
|
748
|
+
console.log('[dsh-flash-proxy] proxy sync from settings/updated: mode=' + newMode)
|
|
749
|
+
}
|
|
750
|
+
if (newCustom !== undefined && newCustom !== _getCustomNoProxy()) {
|
|
751
|
+
_setCustomNoProxy(newCustom)
|
|
752
|
+
}
|
|
753
|
+
if (typeof next.testUrl === 'string' && next.testUrl && next.testUrl !== _getTestUrl()) {
|
|
754
|
+
_setTestUrl(next.testUrl)
|
|
755
|
+
registry.notifyChange('dsh-flash-proxy:test-url')
|
|
756
|
+
console.log('[dsh-flash-proxy] test-url sync from settings/updated: ' + next.testUrl)
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
_fetchProxyStatus()
|
|
760
|
+
})
|
|
761
|
+
return off
|
|
762
|
+
}, 'dsh-flash-proxy: proxy settings sync')
|
|
763
|
+
|
|
764
|
+
// ── Switch 3: test-url ──────────────────────────
|
|
765
|
+
ctx.effect(function () {
|
|
766
|
+
var dispose = registry.registerSwitch({
|
|
767
|
+
id: 'dsh-flash-proxy:test-url',
|
|
768
|
+
label: function () { return t('testUrl') },
|
|
769
|
+
type: 'select',
|
|
770
|
+
cluster: 'system-proxy',
|
|
771
|
+
group: 'system',
|
|
772
|
+
order: 103,
|
|
773
|
+
visible: function () { return _getProxyEnabled() },
|
|
774
|
+
subtitleBlock: true,
|
|
775
|
+
subtitle: function () { return _resolveTestUrl() },
|
|
776
|
+
options: TEST_URL_PRESETS.map(function (p) {
|
|
777
|
+
return { value: p.value, label: p.label }
|
|
778
|
+
}).concat([{ value: 'custom', label: function () { return t('proxyCustom') } }]),
|
|
779
|
+
getValue: function () { return _testUrlPresetValue(_getTestUrl()) },
|
|
780
|
+
setValue: function (v) {
|
|
781
|
+
var url = null
|
|
782
|
+
if (v === 'custom') {
|
|
783
|
+
var entered = prompt(t('testUrlPrompt'), _resolveTestUrl())
|
|
784
|
+
if (entered === null) {
|
|
785
|
+
registry.notifyChange('dsh-flash-proxy:test-url')
|
|
786
|
+
return
|
|
787
|
+
}
|
|
788
|
+
url = String(entered).trim()
|
|
789
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
790
|
+
alert(t('testUrlInvalid'))
|
|
791
|
+
registry.notifyChange('dsh-flash-proxy:test-url')
|
|
792
|
+
return
|
|
793
|
+
}
|
|
794
|
+
} else {
|
|
795
|
+
for (var i = 0; i < TEST_URL_PRESETS.length; i++) {
|
|
796
|
+
if (TEST_URL_PRESETS[i].value === v) url = TEST_URL_PRESETS[i].url
|
|
797
|
+
}
|
|
798
|
+
if (!url) {
|
|
799
|
+
registry.notifyChange('dsh-flash-proxy:test-url')
|
|
800
|
+
return
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
_setTestUrl(url)
|
|
804
|
+
console.log('[dsh-flash-proxy] test url changed: ' + url)
|
|
805
|
+
_suppressHostSync = true
|
|
806
|
+
try {
|
|
807
|
+
var urlSettings = _remoteSettings(ctx)
|
|
808
|
+
if (urlSettings && typeof urlSettings.update === 'function') {
|
|
809
|
+
_queuePrefWrite(ctx, { testUrl: url }, null, {})
|
|
810
|
+
.then(function () {
|
|
811
|
+
_suppressHostSync = false
|
|
812
|
+
_fetchProxyStatus()
|
|
813
|
+
})
|
|
814
|
+
.catch(function (err) {
|
|
815
|
+
console.warn('[dsh-flash-proxy] failed to sync test url to host:', err)
|
|
816
|
+
_suppressHostSync = false
|
|
817
|
+
})
|
|
818
|
+
} else {
|
|
819
|
+
_suppressHostSync = false
|
|
820
|
+
}
|
|
821
|
+
} catch (_) { _suppressHostSync = false }
|
|
822
|
+
},
|
|
823
|
+
})
|
|
824
|
+
return dispose
|
|
825
|
+
}, 'dsh-flash-proxy: test-url switch')
|
|
826
|
+
|
|
827
|
+
// ── Switch 4: test-connection ───────────────────
|
|
828
|
+
ctx.effect(function () {
|
|
829
|
+
var _testing = false
|
|
830
|
+
var dispose = registry.registerSwitch({
|
|
831
|
+
id: 'dsh-flash-proxy:test-connection',
|
|
832
|
+
label: function () { return t('testConnection') },
|
|
833
|
+
hideLabel: true,
|
|
834
|
+
type: 'action',
|
|
835
|
+
cluster: 'system-proxy',
|
|
836
|
+
group: 'system',
|
|
837
|
+
order: 104,
|
|
838
|
+
visible: function () { return _getProxyEnabled() },
|
|
839
|
+
actionLabel: function () { return _testing ? t('testRunning') : t('testConnection') },
|
|
840
|
+
run: function () {
|
|
841
|
+
if (_testing) return
|
|
842
|
+
_testing = true
|
|
843
|
+
var url = _resolveTestUrl()
|
|
844
|
+
_setLog([
|
|
845
|
+
'[' + _logStamp() + '] ▶ ' + _oneLine(url, 300),
|
|
846
|
+
' ' + t('testRunning'),
|
|
847
|
+
])
|
|
848
|
+
_proxyLogMeta = '⏳ ' + t('testRunning')
|
|
849
|
+
registry.notifyChange('dsh-flash-proxy:proxy-log')
|
|
850
|
+
registry.notifyChange('dsh-flash-proxy:test-connection')
|
|
851
|
+
fetch('/plugins/dsh-flash-proxy/test-connection', {
|
|
852
|
+
method: 'POST',
|
|
853
|
+
headers: { 'content-type': 'application/json' },
|
|
854
|
+
body: JSON.stringify({ url: url }),
|
|
855
|
+
})
|
|
856
|
+
.then(function (r) { return r.json() })
|
|
857
|
+
.then(function (data) {
|
|
858
|
+
_testing = false
|
|
859
|
+
_setLog(_describeTest(data))
|
|
860
|
+
if (data && data.ok) {
|
|
861
|
+
_proxyLogMeta = '✅ ' + data.status + ' · ' + data.elapsedMs + 'ms'
|
|
862
|
+
registry.recordChange({
|
|
863
|
+
id: 'dsh-flash-proxy:test-connection',
|
|
864
|
+
label: t('testConnection'),
|
|
865
|
+
icon: '✅',
|
|
866
|
+
oldDisplay: '',
|
|
867
|
+
newDisplay: t('testSuccess') + ' (' + data.elapsedMs + 'ms)',
|
|
868
|
+
})
|
|
869
|
+
} else {
|
|
870
|
+
var err = (data && data.error) || {}
|
|
871
|
+
var errMsg = err.causeCode || err.causeMessage || err.message || 'unknown'
|
|
872
|
+
_proxyLogMeta = '❌ ' + ((data && data.elapsedMs) || 0) + 'ms'
|
|
873
|
+
registry.recordChange({
|
|
874
|
+
id: 'dsh-flash-proxy:test-connection',
|
|
875
|
+
label: t('testConnection'),
|
|
876
|
+
icon: '❌',
|
|
877
|
+
oldDisplay: '',
|
|
878
|
+
newDisplay: t('testFailed') + ': ' + errMsg,
|
|
879
|
+
})
|
|
880
|
+
}
|
|
881
|
+
registry.notifyChange('dsh-flash-proxy:proxy-log')
|
|
882
|
+
registry.notifyChange('dsh-flash-proxy:test-connection')
|
|
883
|
+
})
|
|
884
|
+
.catch(function (e) {
|
|
885
|
+
_testing = false
|
|
886
|
+
var msg = _oneLine(e && e.message ? e.message : String(e), 300)
|
|
887
|
+
_setLog([
|
|
888
|
+
'[' + _logStamp() + '] ▶ ' + _oneLine(url, 300),
|
|
889
|
+
' ' + t('logFailed') + ': ' + msg,
|
|
890
|
+
])
|
|
891
|
+
_proxyLogMeta = '❌ ' + msg.slice(0, 40)
|
|
892
|
+
registry.recordChange({
|
|
893
|
+
id: 'dsh-flash-proxy:test-connection',
|
|
894
|
+
label: t('testConnection'),
|
|
895
|
+
icon: '❌',
|
|
896
|
+
oldDisplay: '',
|
|
897
|
+
newDisplay: t('testFailed') + ': ' + msg,
|
|
898
|
+
})
|
|
899
|
+
registry.notifyChange('dsh-flash-proxy:proxy-log')
|
|
900
|
+
registry.notifyChange('dsh-flash-proxy:test-connection')
|
|
901
|
+
})
|
|
902
|
+
},
|
|
903
|
+
})
|
|
904
|
+
return dispose
|
|
905
|
+
}, 'dsh-flash-proxy: test-connection switch')
|
|
906
|
+
|
|
907
|
+
// ── Switch 5: proxy-log ─────────────────────────
|
|
908
|
+
ctx.effect(function () {
|
|
909
|
+
var dispose = registry.registerSwitch({
|
|
910
|
+
id: 'dsh-flash-proxy:proxy-log',
|
|
911
|
+
label: function () { return t('proxyLog') },
|
|
912
|
+
icon: 'log',
|
|
913
|
+
type: 'log',
|
|
914
|
+
cluster: 'system-proxy',
|
|
915
|
+
group: 'system',
|
|
916
|
+
order: 105,
|
|
917
|
+
visible: function () { return _getProxyEnabled() },
|
|
918
|
+
getLines: function () { return _proxyLog },
|
|
919
|
+
getMeta: function () { return _proxyLogMeta },
|
|
920
|
+
hideWhenEmpty: true,
|
|
921
|
+
clearTitle: function () { return t('proxyLogClear') },
|
|
922
|
+
onClear: function () {
|
|
923
|
+
_proxyLog.length = 0
|
|
924
|
+
_proxyLogMeta = ''
|
|
925
|
+
registry.notifyChange('dsh-flash-proxy:proxy-log')
|
|
926
|
+
},
|
|
927
|
+
})
|
|
928
|
+
return dispose
|
|
929
|
+
}, 'dsh-flash-proxy: proxy-log switch')
|
|
930
|
+
|
|
931
|
+
// ── Switch 6: proxy-env ───────────────────────
|
|
932
|
+
ctx.effect(function () {
|
|
933
|
+
var dispose = registry.registerSwitch({
|
|
934
|
+
id: 'dsh-flash-proxy:proxy-env',
|
|
935
|
+
label: function () { return t('proxyEnv') },
|
|
936
|
+
icon: 'queue',
|
|
937
|
+
type: 'log',
|
|
938
|
+
cluster: 'system-proxy',
|
|
939
|
+
group: 'system',
|
|
940
|
+
order: 102,
|
|
941
|
+
visible: function () { return _getProxyEnabled() },
|
|
942
|
+
getLines: function () {
|
|
943
|
+
var env = _proxyEnvValue || {}
|
|
944
|
+
var unset = '(' + t('noProxyNotSet') + ')'
|
|
945
|
+
var lines = []
|
|
946
|
+
if (_proxyEnvStale) lines.push('⟳ ' + t('proxyEnvRefreshing'))
|
|
947
|
+
lines.push('HTTP_PROXY' + '=' + (env.http || unset))
|
|
948
|
+
lines.push('HTTPS_PROXY' + '=' + (env.https || unset))
|
|
949
|
+
lines.push('ALL_PROXY' + '=' + (env.all || unset))
|
|
950
|
+
lines.push('NO_PROXY' + '=' + ((_noProxyValue === null ? null : _noProxyValue) || unset))
|
|
951
|
+
return lines
|
|
952
|
+
},
|
|
953
|
+
getMeta: function () { return _proxyEnvStale ? '⟳' : '' },
|
|
954
|
+
})
|
|
955
|
+
return dispose
|
|
956
|
+
}, 'dsh-flash-proxy: proxy-env switch')
|
|
957
|
+
} // end registerProxySwitches
|
|
958
|
+
|
|
959
|
+
// ── Triple-discovery: three paths to the quickControl registry ──
|
|
960
|
+
//
|
|
961
|
+
// 1. ctx.inject(['quickControl']) — the reliable path. Cordis calls
|
|
962
|
+
// the callback once the service is available, even if it arrives
|
|
963
|
+
// later (e.g. dock-flash loads after us). This is the primary path.
|
|
964
|
+
//
|
|
965
|
+
// 2. dock-flash:ready event — dock-flash emits this when its
|
|
966
|
+
// QuickControl registry is ready. Good as a secondary signal.
|
|
967
|
+
//
|
|
968
|
+
// 3. ctx.get('quickControl') sync check — catches the case where the
|
|
969
|
+
// service is already available *right now* but inject hasn't fired
|
|
970
|
+
// yet (edge case on warm reload).
|
|
971
|
+
//
|
|
972
|
+
// The _registered flag prevents double-registration across paths.
|
|
973
|
+
function safeRegister(registry) {
|
|
974
|
+
if (_registered) return
|
|
975
|
+
if (!registry || typeof registry.registerSwitch !== 'function') return
|
|
976
|
+
_registered = true
|
|
977
|
+
registerProxySwitches(registry)
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Path 1: inject-based (primary — guaranteed by Cordis lifecycle)
|
|
981
|
+
ctx.inject(['quickControl'], function (qcCtx) {
|
|
982
|
+
var registry = qcCtx.get('quickControl')
|
|
983
|
+
if (registry) safeRegister(registry)
|
|
984
|
+
})
|
|
985
|
+
|
|
986
|
+
// Path 2: event-based (secondary — dock-flash specific)
|
|
987
|
+
// dock-flash:ready payload is { quickControl: registry, alerts: alertRegistry }
|
|
988
|
+
ctx.on('dock-flash:ready', function (payload) {
|
|
989
|
+
var registry = payload && payload.quickControl ? payload.quickControl : payload
|
|
990
|
+
safeRegister(registry)
|
|
991
|
+
})
|
|
992
|
+
|
|
993
|
+
// Path 3: sync check (tertiary — warm reload edge case)
|
|
994
|
+
var registryNow = ctx.get('quickControl')
|
|
995
|
+
if (registryNow) safeRegister(registryNow)
|
|
996
|
+
}
|
|
997
|
+
return module.exports
|
|
998
|
+
},
|
|
999
|
+
})
|