@goodandready/dsh-lanmode 0.1.0 → 0.2.0
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/README.md +7 -23
- package/lib/index.js +44 -12
- package/lib/shim.js +123 -0
- package/package.json +4 -10
- package/lib/client.js +0 -253
package/README.md
CHANGED
|
@@ -28,34 +28,18 @@ Every bound namespace reports `status: "unavailable"` for the life of the page,
|
|
|
28
28
|
|
|
29
29
|
## What the plugin does
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
It injects one script into the served `index.html`, through the web server's official index tap, ahead of the boot manifest. The script intercepts module registration and hands the two settings packages a connection whose `isLoopback` reads `true`. Everything downstream — the shared mirror of the settings document, every namespace scope, the core's own settings pages — then behaves exactly as it does on localhost.
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
The substitution is narrow on purpose. Three packages read that flag, and the third is deliverables, where it decides whether a produced file may be opened locally: forcing it there would ask the Host to open paths on the server's desktop. Only the settings packages see the substitute; the rest of the UI keeps the truth.
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
- and, if the runtime allows a plugin to claim the name, in place of `settingsScope` — which repairs every settings surface at once, including the core's plugin configuration tab and plugins that know nothing about this one.
|
|
35
|
+
Taking the core settings service over instead is not possible — cordis refuses a second provider for a registered name, and refuses assignment across fibers.
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
## Checking it
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
- `?lanmode=invert` — report the flag as `false` even on a loopback page. The harness then fails exactly the way it does over the LAN, which is how this plugin is verified.
|
|
40
|
+
- `?lanmode=off` — stand the plugin down entirely.
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
dsh plugin --profile web add @goodandready/dsh-lanmode
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Restart the Web UI afterwards, then reload the browser.
|
|
47
|
-
|
|
48
|
-
## Checking it on a machine where it is not needed
|
|
49
|
-
|
|
50
|
-
The plugin stands aside on a loopback page, which makes it awkward to try out
|
|
51
|
-
on the machine that runs the harness. A debug switch turns it on there anyway:
|
|
52
|
-
|
|
53
|
-
```js
|
|
54
|
-
localStorage.setItem('dsh-lanmode:force', '1'); location.reload()
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Remove the key to go back to normal. This is only for looking at the plugin
|
|
58
|
-
itself; nothing in day-to-day use needs it.
|
|
42
|
+
On a loopback page the plugin substitutes `true` where the real value is already `true`, so it cannot change behaviour there.
|
|
59
43
|
|
|
60
44
|
## What it is not
|
|
61
45
|
|
package/lib/index.js
CHANGED
|
@@ -1,20 +1,52 @@
|
|
|
1
1
|
// dsh-lanmode — хост-половина.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// в клиентском коде харнесса, а сервер и без него отдаёт настройки по сети.
|
|
5
|
-
// Хост-половина существует потому, что запись в дереве плагинов — это модуль
|
|
6
|
-
// на хосте; она ничего не регистрирует и ничего не слушает.
|
|
3
|
+
// Задача плагина: вернуть настройки на страницах, открытых не с localhost.
|
|
7
4
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
5
|
+
// Веб-интерфейс решает это в браузере, по имени хоста страницы:
|
|
6
|
+
//
|
|
7
|
+
// isLoopback: pageLocation === undefined || isLoopbackHostname(hostname)
|
|
8
|
+
//
|
|
9
|
+
// где своими считаются только localhost, [::1] и 127.0.0.0/8. Дальше сервис
|
|
10
|
+
// настроек уходит в режим «памяти»: общий вид документа не читается никогда,
|
|
11
|
+
// каждый раздел получает статус "unavailable", запись выбрасывается до
|
|
12
|
+
// отправки. Внешне — пустые карточки всех плагинов, пустая вкладка «Настройки
|
|
13
|
+
// плагинов» и страница «Модели» с надписью
|
|
14
|
+
// "settings are unavailable in this browser".
|
|
15
|
+
//
|
|
16
|
+
// Сервер это ограничение не разделяет: settings.describe и settings.mutate по
|
|
17
|
+
// сети отвечают штатно, когда Origin совпадает с адресом страницы, — то есть
|
|
18
|
+
// ровно в том случае, когда запрос шлёт сам интерфейс.
|
|
19
|
+
//
|
|
20
|
+
// Поэтому плагин вставляет в index.html короткий скрипт (через официальную
|
|
21
|
+
// точку webServer.tapIndex) и подменяет флаг ТОЧЕЧНО — только для двух
|
|
22
|
+
// настроечных пакетов ядра. Флаг читают трое, и третьему подменять нельзя:
|
|
23
|
+
// в результатах работы isLoopback решает, можно ли открыть файл локально, и
|
|
24
|
+
// с подменой браузер просил бы открыть путь на машине сервера.
|
|
25
|
+
|
|
26
|
+
import { readFileSync } from 'node:fs'
|
|
27
|
+
import path from 'node:path'
|
|
28
|
+
import { fileURLToPath } from 'node:url'
|
|
10
29
|
|
|
11
30
|
export const name = 'dsh-lanmode'
|
|
12
|
-
export const inject = []
|
|
31
|
+
export const inject = ['webServer']
|
|
32
|
+
|
|
33
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
34
|
+
|
|
35
|
+
/** Скрипт-заплатка читается с диска: так его видно и правится он как код. */
|
|
36
|
+
function shimSource() {
|
|
37
|
+
return readFileSync(path.join(here, 'shim.js'), 'utf8')
|
|
38
|
+
}
|
|
13
39
|
|
|
14
40
|
export function apply(ctx) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
41
|
+
const script = '<script data-dsh-lanmode="1">' + shimSource() + '</script>'
|
|
42
|
+
|
|
43
|
+
ctx.effect(() => ctx.webServer.tapIndex((html) => {
|
|
44
|
+
if (html.includes('data-dsh-lanmode')) return html
|
|
45
|
+
// Сразу за <head>: к моменту, когда страница начнёт грузить бандлы,
|
|
46
|
+
// перехватчик уже стоит. Загрузчик к этому моменту может ещё не
|
|
47
|
+
// существовать — заплатка это учитывает.
|
|
48
|
+
const at = html.indexOf('<head>')
|
|
49
|
+
if (at === -1) return script + html
|
|
50
|
+
return html.slice(0, at + '<head>'.length) + script + html.slice(at + '<head>'.length)
|
|
51
|
+
}), 'dsh-lanmode: вставка заплатки в index.html')
|
|
20
52
|
}
|
package/lib/shim.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
// Заплатка выполняется в самом начале страницы, до загрузки бандлов ядра.
|
|
3
|
+
//
|
|
4
|
+
// Она перехватывает регистрацию двух настроечных пакетов и подсовывает им
|
|
5
|
+
// связь, у которой isLoopback всегда true. Остальные пакеты видят правду:
|
|
6
|
+
// в результатах работы этот же флаг решает, открывать ли файл локально, и
|
|
7
|
+
// подменять его там нельзя.
|
|
8
|
+
var TARGETS = [
|
|
9
|
+
'@deepseek-ai/dsh-client-ui-settings',
|
|
10
|
+
'@deepseek-ai/dsh-client-ui-settings-general',
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
// Отладочные переключатели через адресную строку:
|
|
14
|
+
// ?lanmode=off — не вмешиваться (проверить, как было без плагина);
|
|
15
|
+
// ?lanmode=invert — притвориться чужим на своей же странице
|
|
16
|
+
// (так проверяется, что заплатка вообще управляет флагом).
|
|
17
|
+
var mode = ''
|
|
18
|
+
try { mode = new URLSearchParams(location.search).get('lanmode') || '' } catch (noSearch) { mode = '' }
|
|
19
|
+
if (mode === 'off') return
|
|
20
|
+
var forced = mode === 'invert' ? false : true
|
|
21
|
+
|
|
22
|
+
function connectionWithForcedFlag(connection) {
|
|
23
|
+
if (!connection || typeof connection !== 'object') return connection
|
|
24
|
+
return new Proxy(connection, {
|
|
25
|
+
get: function (target, prop, receiver) {
|
|
26
|
+
if (prop === 'isLoopback') return forced
|
|
27
|
+
var value = Reflect.get(target, prop, target)
|
|
28
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ctxWithForcedConnection(ctx) {
|
|
34
|
+
return new Proxy(ctx, {
|
|
35
|
+
get: function (target, prop, receiver) {
|
|
36
|
+
if (prop === 'get') {
|
|
37
|
+
return function (nameRequested) {
|
|
38
|
+
var value = target.get(nameRequested)
|
|
39
|
+
return nameRequested === 'connection' ? connectionWithForcedFlag(value) : value
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (prop === 'connection') return connectionWithForcedFlag(Reflect.get(target, prop, target))
|
|
43
|
+
var value = Reflect.get(target, prop, target)
|
|
44
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function wrap(registration) {
|
|
50
|
+
if (!registration || TARGETS.indexOf(registration.id) === -1) return registration
|
|
51
|
+
if (typeof registration.factory !== 'function') return registration
|
|
52
|
+
var factory = registration.factory
|
|
53
|
+
var patched = {}
|
|
54
|
+
for (var key in registration) patched[key] = registration[key]
|
|
55
|
+
patched.factory = function (requireFn) {
|
|
56
|
+
var moduleExports = factory(requireFn)
|
|
57
|
+
if (!moduleExports || typeof moduleExports.apply !== 'function') return moduleExports
|
|
58
|
+
var originalApply = moduleExports.apply
|
|
59
|
+
moduleExports.apply = function (ctx) {
|
|
60
|
+
return originalApply.call(this, ctxWithForcedConnection(ctx))
|
|
61
|
+
}
|
|
62
|
+
return moduleExports
|
|
63
|
+
}
|
|
64
|
+
return patched
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Загрузчик подменяет собственный load, когда переходит из режима очереди
|
|
68
|
+
// в рабочий, — обёртка при этом слетала бы. Поэтому ставим её через
|
|
69
|
+
// свойство-аксессор: любая новая функция оборачивается снова.
|
|
70
|
+
function install(loader) {
|
|
71
|
+
if (!loader || typeof loader !== 'object') return loader
|
|
72
|
+
var wrapped
|
|
73
|
+
|
|
74
|
+
function rewrap(fn) {
|
|
75
|
+
if (typeof fn !== 'function' || fn.__dshLanmode) { wrapped = fn; return }
|
|
76
|
+
var inner = fn
|
|
77
|
+
wrapped = function (registration) { return inner.call(this, wrap(registration)) }
|
|
78
|
+
wrapped.__dshLanmode = true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (loader.load && loader.load.__dshLanmode) return loader
|
|
82
|
+
rewrap(loader.load)
|
|
83
|
+
try {
|
|
84
|
+
Object.defineProperty(loader, 'load', {
|
|
85
|
+
configurable: true,
|
|
86
|
+
get: function () { return wrapped },
|
|
87
|
+
set: function (fn) { rewrap(fn) },
|
|
88
|
+
})
|
|
89
|
+
} catch (cannotDefine) {
|
|
90
|
+
loader.load = wrapped
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Уже поставленные в очередь до нас — тоже наши.
|
|
94
|
+
if (Array.isArray(loader.pendingQueue)) {
|
|
95
|
+
for (var i = 0; i < loader.pendingQueue.length; i++) {
|
|
96
|
+
loader.pendingQueue[i] = wrap(loader.pendingQueue[i])
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
console.info('[dsh-lanmode] настройки будут работать на этой странице'
|
|
101
|
+
+ (forced ? '' : ' (перевёрнутый режим: притворяемся чужим)'))
|
|
102
|
+
} catch (noConsole) { /* незачем */ }
|
|
103
|
+
return loader
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Загрузчик объявляется встроенным скриптом страницы. Если он уже есть —
|
|
107
|
+
// оборачиваем сразу; если нет — дожидаемся присваивания.
|
|
108
|
+
if (window.__ModuleLoader__) {
|
|
109
|
+
install(window.__ModuleLoader__)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
var held
|
|
113
|
+
try {
|
|
114
|
+
Object.defineProperty(window, '__ModuleLoader__', {
|
|
115
|
+
configurable: true,
|
|
116
|
+
get: function () { return held },
|
|
117
|
+
set: function (value) { held = install(value) },
|
|
118
|
+
})
|
|
119
|
+
} catch (cannotDefine) {
|
|
120
|
+
// Свойство неподатливо — молчим: без заплатки страница остаётся такой же,
|
|
121
|
+
// какой была без плагина.
|
|
122
|
+
}
|
|
123
|
+
})()
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-lanmode",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Settings over the LAN for DeepSeek Harness: the Web UI turns
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Settings over the LAN for DeepSeek Harness: the Web UI turns its settings service off on any page that is not localhost, even though the server serves settings over the network fine. This plugin taps the served index.html and hands the two settings packages a connection that reports loopback, so every settings surface works from any address.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./lib/index.js",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./lib/index.js",
|
|
10
|
-
"./client": "./lib/client.js",
|
|
11
10
|
"./package.json": "./package.json",
|
|
12
11
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
13
12
|
},
|
|
@@ -39,15 +38,10 @@
|
|
|
39
38
|
"dsh": {
|
|
40
39
|
"bundle": {
|
|
41
40
|
"patch": "./cordis.patch.yml"
|
|
42
|
-
},
|
|
43
|
-
"client": {
|
|
44
|
-
"platform": "web",
|
|
45
|
-
"inject": [
|
|
46
|
-
"@deepseek-ai/dsh-client-runtime"
|
|
47
|
-
]
|
|
48
41
|
}
|
|
49
42
|
},
|
|
50
43
|
"peerDependencies": {
|
|
51
|
-
"@deepseek-ai/cordis": "^4.0.1"
|
|
44
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
45
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6"
|
|
52
46
|
}
|
|
53
47
|
}
|
package/lib/client.js
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
// dsh-lanmode — клиентская половина.
|
|
2
|
-
//
|
|
3
|
-
// Зачем это существует.
|
|
4
|
-
//
|
|
5
|
-
// Веб-интерфейс харнесса отключает настройки на любой странице, открытой не с
|
|
6
|
-
// localhost. Решение принимается в браузере по имени хоста:
|
|
7
|
-
//
|
|
8
|
-
// isLoopback: pageLocation === undefined || isLoopbackHostname(pageLocation.hostname)
|
|
9
|
-
//
|
|
10
|
-
// и дальше сервис настроек уходит в режим «памяти»: общее зеркало документа
|
|
11
|
-
// никогда не читается, каждый связанный раздел сразу получает статус
|
|
12
|
-
// "unavailable", а запись молча выбрасывается. Внешне это выглядит как пустые
|
|
13
|
-
// карточки настроек у всех плагинов сразу и пустая вкладка «Настройки
|
|
14
|
-
// плагинов» — при полностью исправном сервере.
|
|
15
|
-
//
|
|
16
|
-
// Сервер этого ограничения не разделяет: и чтение (settings.describe), и
|
|
17
|
-
// запись (settings.mutate) по сети работают штатно, если Origin совпадает с
|
|
18
|
-
// адресом страницы — то есть ровно в том случае, когда запрос шлёт сам
|
|
19
|
-
// интерфейс. Проверено на живом харнессе через обратный прокси.
|
|
20
|
-
//
|
|
21
|
-
// Поэтому плагин делает одно: на не-loopback странице поднимает собственный
|
|
22
|
-
// экземпляр той же механики поверх тех же вызовов и отдаёт его как службу.
|
|
23
|
-
// На loopback-странице он не делает ничего — там ядро справляется само.
|
|
24
|
-
|
|
25
|
-
window.__ModuleLoader__.load({
|
|
26
|
-
id: '@goodandready/dsh-lanmode',
|
|
27
|
-
factory: (require) => {
|
|
28
|
-
var module = { exports: {} }
|
|
29
|
-
var exports = module.exports
|
|
30
|
-
|
|
31
|
-
// ------------------------------------------------- зеркало документа
|
|
32
|
-
//
|
|
33
|
-
// Один читатель settings.describe на всю страницу: разделы выводятся из
|
|
34
|
-
// него, поэтому они не могут разойтись во мнении о документе.
|
|
35
|
-
function createMirror(api) {
|
|
36
|
-
let snapshot = { status: 'idle', view: undefined, error: null }
|
|
37
|
-
const listeners = new Set()
|
|
38
|
-
let inFlight
|
|
39
|
-
let rerun = false
|
|
40
|
-
|
|
41
|
-
const notify = () => {
|
|
42
|
-
for (const listener of [...listeners]) {
|
|
43
|
-
try { listener() } catch (listenerFailure) { /* чужой слушатель нам не судья */ }
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
const put = (next) => { snapshot = next; notify() }
|
|
47
|
-
|
|
48
|
-
async function run() {
|
|
49
|
-
do {
|
|
50
|
-
rerun = false
|
|
51
|
-
let outcome
|
|
52
|
-
try {
|
|
53
|
-
const response = await api.settings.describe({})
|
|
54
|
-
outcome = response.result.ok
|
|
55
|
-
? { view: response.result.value }
|
|
56
|
-
: { failure: response.result.error.message }
|
|
57
|
-
} catch (wireFailure) {
|
|
58
|
-
outcome = { failure: String(wireFailure && wireFailure.message || wireFailure) }
|
|
59
|
-
}
|
|
60
|
-
if (outcome.view !== undefined) {
|
|
61
|
-
put({ status: 'ready', view: outcome.view, error: null })
|
|
62
|
-
} else {
|
|
63
|
-
// Держим то, что уже прочитали: неудачное обновление не должно
|
|
64
|
-
// опустошать готовые разделы.
|
|
65
|
-
put({ status: snapshot.view === undefined ? 'idle' : 'ready', view: snapshot.view, error: outcome.failure })
|
|
66
|
-
}
|
|
67
|
-
} while (rerun)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
getSnapshot: () => snapshot,
|
|
72
|
-
subscribe: (listener) => {
|
|
73
|
-
listeners.add(listener)
|
|
74
|
-
return () => { listeners.delete(listener) }
|
|
75
|
-
},
|
|
76
|
-
load() {
|
|
77
|
-
if (inFlight !== undefined) { rerun = true; return inFlight }
|
|
78
|
-
inFlight = run().finally(() => { inFlight = undefined })
|
|
79
|
-
return inFlight
|
|
80
|
-
},
|
|
81
|
-
ensure() {
|
|
82
|
-
if (inFlight !== undefined) return inFlight
|
|
83
|
-
if (snapshot.status === 'idle') return this.load()
|
|
84
|
-
return Promise.resolve()
|
|
85
|
-
},
|
|
86
|
-
// Ответ на запись возвращает свежий вид одного раздела — вкладываем его
|
|
87
|
-
// на место, чтобы не перечитывать весь документ ради одного поля.
|
|
88
|
-
acceptView(view) {
|
|
89
|
-
const before = snapshot
|
|
90
|
-
if (before.view === undefined) { this.load(); return }
|
|
91
|
-
const known = before.view.namespaces.some((row) => row.ns === view.ns)
|
|
92
|
-
const namespaces = known
|
|
93
|
-
? before.view.namespaces.map((row) => (row.ns === view.ns ? view : row))
|
|
94
|
-
: before.view.namespaces.concat([view])
|
|
95
|
-
put({ ...before, view: { ...before.view, namespaces } })
|
|
96
|
-
},
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ------------------------------------------------------- раздел настроек
|
|
101
|
-
//
|
|
102
|
-
// Снимок повторяет форму ядрового: status, value, base, user, revision,
|
|
103
|
-
// writable. Карточки читают именно эти поля, поэтому подмена для них
|
|
104
|
-
// незаметна.
|
|
105
|
-
function createScope(api, mirror, namespace) {
|
|
106
|
-
let snapshot = {
|
|
107
|
-
status: 'loading',
|
|
108
|
-
value: undefined,
|
|
109
|
-
base: undefined,
|
|
110
|
-
user: undefined,
|
|
111
|
-
revision: undefined,
|
|
112
|
-
writable: false,
|
|
113
|
-
mode: 'host',
|
|
114
|
-
}
|
|
115
|
-
const listeners = new Set()
|
|
116
|
-
const notify = () => {
|
|
117
|
-
for (const listener of [...listeners]) {
|
|
118
|
-
try { listener() } catch (listenerFailure) { /* см. выше */ }
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function derive() {
|
|
123
|
-
const held = mirror.getSnapshot()
|
|
124
|
-
if (held.view === undefined) return
|
|
125
|
-
const row = held.view.namespaces.find((candidate) => candidate.ns === namespace)
|
|
126
|
-
if (row === undefined) {
|
|
127
|
-
// Хост про такой раздел не знает. Это законное состояние: плагин
|
|
128
|
-
// может быть ещё не применён.
|
|
129
|
-
snapshot = { ...snapshot, status: 'unavailable', writable: held.view.writable }
|
|
130
|
-
notify()
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
snapshot = {
|
|
134
|
-
status: 'ready',
|
|
135
|
-
value: row.value,
|
|
136
|
-
base: row.base,
|
|
137
|
-
user: row.user,
|
|
138
|
-
revision: row.revision,
|
|
139
|
-
writable: held.view.writable,
|
|
140
|
-
mode: 'host',
|
|
141
|
-
}
|
|
142
|
-
notify()
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const off = mirror.subscribe(derive)
|
|
146
|
-
derive()
|
|
147
|
-
|
|
148
|
-
// Записи выстраиваем в очередь: правка ревизии, пришедшая из ответа,
|
|
149
|
-
// должна попасть в следующий запрос, иначе хост отвергнет его как
|
|
150
|
-
// устаревший.
|
|
151
|
-
let tail = Promise.resolve()
|
|
152
|
-
function write(op) {
|
|
153
|
-
const task = tail.then(async () => {
|
|
154
|
-
const revision = snapshot.revision
|
|
155
|
-
let response
|
|
156
|
-
try {
|
|
157
|
-
response = await api.settings.mutate({
|
|
158
|
-
ns: namespace,
|
|
159
|
-
ops: [op],
|
|
160
|
-
...(revision === undefined ? {} : { expectedRevision: revision }),
|
|
161
|
-
})
|
|
162
|
-
} catch (wireFailure) {
|
|
163
|
-
await mirror.load()
|
|
164
|
-
throw wireFailure
|
|
165
|
-
}
|
|
166
|
-
if (!response.result.ok) {
|
|
167
|
-
// Чаще всего это разошедшаяся ревизия: перечитываем и отдаём
|
|
168
|
-
// ошибку наверх, чтобы карточка показала неудачу сохранения.
|
|
169
|
-
await mirror.load()
|
|
170
|
-
throw new Error(response.result.error.message)
|
|
171
|
-
}
|
|
172
|
-
mirror.acceptView(response.result.value)
|
|
173
|
-
})
|
|
174
|
-
tail = task.catch(() => {})
|
|
175
|
-
return task
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
getSnapshot: () => snapshot,
|
|
180
|
-
subscribe: (listener) => {
|
|
181
|
-
listeners.add(listener)
|
|
182
|
-
return () => { listeners.delete(listener) }
|
|
183
|
-
},
|
|
184
|
-
set: (field, value) => write({ op: 'set', path: [field], value }),
|
|
185
|
-
unset: (field) => write({ op: 'unset', path: [field] }),
|
|
186
|
-
dispose: () => { off() },
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
exports.inject = ['connection']
|
|
191
|
-
|
|
192
|
-
// Отладочный переключатель: включает режим и на loopback-странице, где
|
|
193
|
-
// ядро и без нас справляется. Нужен, чтобы проверить сам плагин там, где
|
|
194
|
-
// это удобно сделать:
|
|
195
|
-
//
|
|
196
|
-
// localStorage.setItem('dsh-lanmode:force', '1'); location.reload()
|
|
197
|
-
//
|
|
198
|
-
// В обычной работе не нужен и по умолчанию выключен.
|
|
199
|
-
function forcedOn() {
|
|
200
|
-
try { return window.localStorage.getItem('dsh-lanmode:force') === '1' }
|
|
201
|
-
catch (noStorage) { return false }
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
exports.apply = function apply(ctx) {
|
|
205
|
-
const connection = ctx.get('connection')
|
|
206
|
-
|
|
207
|
-
// На loopback-странице ядро работает штатно, и вмешиваться незачем.
|
|
208
|
-
if (connection.isLoopback && !forcedOn()) return
|
|
209
|
-
|
|
210
|
-
const mirror = createMirror(connection.api)
|
|
211
|
-
const binder = {
|
|
212
|
-
bind: (spec) => createScope(connection.api, mirror, spec && spec.namespace),
|
|
213
|
-
describe: () => mirror,
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Документ меняют не только из этой вкладки; ядро слушает тот же сигнал.
|
|
217
|
-
try {
|
|
218
|
-
const remote = ctx.get('remote')
|
|
219
|
-
if (remote && typeof remote.$on === 'function') {
|
|
220
|
-
ctx.effect(() => remote.$on('settings/document-updated', () => { mirror.load() }),
|
|
221
|
-
'dsh-lanmode: перечитывать документ настроек по сигналу хоста')
|
|
222
|
-
}
|
|
223
|
-
} catch (noRemoteService) { /* без сигнала обойдёмся, останется ручное перечитывание */ }
|
|
224
|
-
|
|
225
|
-
mirror.ensure()
|
|
226
|
-
|
|
227
|
-
// Своя служба: ею может пользоваться любой плагин, которому нужны
|
|
228
|
-
// настройки по сети.
|
|
229
|
-
ctx.provide('lanSettings', binder)
|
|
230
|
-
|
|
231
|
-
// Попытка встать на место ядровой службы. Если разрешено — оживает весь
|
|
232
|
-
// интерфейс настроек разом, включая чужие плагины и вкладку ядра.
|
|
233
|
-
// Если ядро не отдаёт имя, остаёмся со своей службой: наши плагины
|
|
234
|
-
// умеют её спрашивать.
|
|
235
|
-
let tookOver = false
|
|
236
|
-
try {
|
|
237
|
-
ctx.provide('settingsScope', binder)
|
|
238
|
-
tookOver = true
|
|
239
|
-
} catch (nameTaken) {
|
|
240
|
-
tookOver = false
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Одна строка в консоли: без неё непонятно, работает режим или нет.
|
|
244
|
-
try {
|
|
245
|
-
console.info('[dsh-lanmode] режим включён (' + location.hostname + (connection.isLoopback ? ', принудительно' : '') + '); '
|
|
246
|
-
+ 'настройки подняты через сеть, ядровая служба '
|
|
247
|
-
+ (tookOver ? 'подменена' : 'оставлена как есть'))
|
|
248
|
-
} catch (noConsole) { /* незачем */ }
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return module.exports
|
|
252
|
-
},
|
|
253
|
-
})
|