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/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
+ })