dsh-builtin-browser 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +84 -39
- package/README.md +78 -38
- package/client.js +2 -3
- package/cordis.patch.yml +13 -7
- package/desktop-bridge/install.mjs +274 -12
- package/desktop-bridge/plugin-browser-bridge.js +55 -15
- package/docs/architecture.md +1 -1
- package/docs/tool-reference.md +8 -8
- package/docs/user-guide.md +4 -2
- package/docs/why-browser.md +1 -1
- package/lib/browser-electron/desktop-bridge-host.d.ts +10 -1
- package/lib/browser-electron/desktop-bridge-host.js +10 -2
- package/lib/browser-electron/entry.js +22 -2
- package/lib/browser-electron/history-store.js +5 -1
- package/lib/browser-electron/missing-system-browser.d.ts +54 -0
- package/lib/browser-electron/missing-system-browser.js +89 -0
- package/lib/browser-electron/provider.d.ts +54 -1
- package/lib/browser-electron/provider.js +241 -37
- package/lib/browser-electron/remote-host.js +6 -3
- package/lib/browser-electron/settings-store.js +10 -4
- package/lib/browser-electron/system-browser.d.ts +27 -6
- package/lib/browser-electron/system-browser.js +274 -37
- package/lib/tool-browser/index.d.ts +14 -0
- package/lib/tool-browser/index.js +143 -58
- package/package.json +1 -1
- package/src/browser-electron/desktop-bridge-host.ts +11 -2
- package/src/browser-electron/entry.ts +22 -2
- package/src/browser-electron/history-store.ts +5 -1
- package/src/browser-electron/missing-system-browser.ts +113 -0
- package/src/browser-electron/provider.ts +267 -36
- package/src/browser-electron/remote-host.ts +6 -3
- package/src/browser-electron/settings-store.ts +11 -4
- package/src/browser-electron/system-browser.ts +256 -31
- package/src/tool-browser/index.ts +149 -63
|
@@ -8,31 +8,92 @@
|
|
|
8
8
|
* expressed as something replayable rather than as a one-off edit — re-run this
|
|
9
9
|
* after every desktop upgrade.
|
|
10
10
|
*
|
|
11
|
+
* TWO LAYOUTS
|
|
12
|
+
* DSH Desktop ships in two shapes, and they need different work:
|
|
13
|
+
*
|
|
14
|
+
* unpacked `<install>/resources/app/lib/main.js` is a real file. The
|
|
15
|
+
* long-standing path: copy the module next to main.js and append a
|
|
16
|
+
* guarded import to it.
|
|
17
|
+
*
|
|
18
|
+
* packed `<install>/resources/app.asar` holds the whole app (main.js is
|
|
19
|
+
* inside the archive) and `resources/app.asar.unpacked` holds the
|
|
20
|
+
* entries marked `unpacked`, i.e. the native modules. The official
|
|
21
|
+
* Windows build is packed — where this script used to fail with
|
|
22
|
+
* "not a DSH Desktop install" on a perfectly good installation,
|
|
23
|
+
* because it only ever looked for the unpacked main.js.
|
|
24
|
+
*
|
|
25
|
+
* HOW THE PACKED LAYOUT IS HANDLED
|
|
26
|
+
* Electron loads `app.asar` in preference to `app/`, and a directory only wins
|
|
27
|
+
* once the archive is not there. Rewriting the archive in place is not an
|
|
28
|
+
* option either: the official build enables the
|
|
29
|
+
* `EnableEmbeddedAsarIntegrityValidation` fuse (measured: enabled), so a
|
|
30
|
+
* modified app.asar is a broken app. The archive is therefore never touched:
|
|
31
|
+
*
|
|
32
|
+
* 1. extract `app.asar` into `resources/app`, taking the real bytes of
|
|
33
|
+
* `unpacked` entries from `resources/app.asar.unpacked` (a straight
|
|
34
|
+
* archive walk would leave the native modules as empty stubs);
|
|
35
|
+
* 2. install the bridge into that tree exactly as in the unpacked layout;
|
|
36
|
+
* 3. rename `app.asar` to `app.asar.before-bridge`, so Electron falls back to
|
|
37
|
+
* the directory that was just built.
|
|
38
|
+
*
|
|
39
|
+
* The trigger is the ARCHIVE's presence, not the directory's absence. A desktop
|
|
40
|
+
* update writes a new `app.asar` and leaves any earlier extraction lying around
|
|
41
|
+
* (the updater does not know about that directory), so keying off "the tree is
|
|
42
|
+
* missing" would patch the stale tree while Electron keeps loading the archive —
|
|
43
|
+
* a run that reports success with the bridge dead. Whenever the archive is
|
|
44
|
+
* there it is the app, so the tree is rebuilt from it.
|
|
45
|
+
*
|
|
46
|
+
* `--revert` moves the archive back and restores the pristine main.js. The
|
|
47
|
+
* extracted tree then becomes inert (the archive wins again) and can be deleted.
|
|
48
|
+
*
|
|
11
49
|
* WHAT IT DOES (idempotent)
|
|
12
50
|
* 1. copies `plugin-browser-bridge.js` next to the app's main bundle;
|
|
13
51
|
* 2. appends a guarded import to `lib/main.js` that starts it after `whenReady`,
|
|
14
52
|
* keeping a pristine `main.js.before-bridge` backup the first time;
|
|
15
|
-
* 3. does nothing if the import is already present
|
|
53
|
+
* 3. does nothing if the import is already present;
|
|
54
|
+
* 4. keeps `dsh <args>` working: the Desktop CLI launcher and the CLI's own
|
|
55
|
+
* runtime lookup both spell `app.asar` out loud and both break once the
|
|
56
|
+
* archive is moved aside, so both are patched (each with its own
|
|
57
|
+
* `.before-bridge` backup) and `--revert` restores them.
|
|
16
58
|
*
|
|
17
59
|
* USAGE
|
|
18
|
-
* node
|
|
60
|
+
* node desktop-bridge/install.mjs [path-to-DeepSeek-Harness-install]
|
|
19
61
|
* Default path: %LOCALAPPDATA%\Programs\DeepSeek Harness
|
|
20
|
-
* Pass --revert to restore the pre-bridge
|
|
62
|
+
* Pass --revert to restore the pre-bridge state instead.
|
|
21
63
|
*/
|
|
22
|
-
import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
23
|
-
import { join } from 'node:path'
|
|
64
|
+
import { copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
65
|
+
import { dirname, join } from 'node:path'
|
|
24
66
|
|
|
25
67
|
const DEFAULT_INSTALL = join(process.env.LOCALAPPDATA ?? '', 'Programs', 'DeepSeek Harness')
|
|
26
68
|
const args = process.argv.slice(2)
|
|
27
69
|
const revert = args.includes('--revert')
|
|
28
70
|
const installRoot = args.find(arg => !arg.startsWith('--')) ?? DEFAULT_INSTALL
|
|
29
71
|
|
|
30
|
-
const
|
|
72
|
+
const resourcesDir = join(installRoot, 'resources')
|
|
73
|
+
const appDir = join(resourcesDir, 'app')
|
|
74
|
+
const asarPath = join(resourcesDir, 'app.asar')
|
|
75
|
+
const asarMoved = `${asarPath}.before-bridge`
|
|
31
76
|
const mainPath = join(appDir, 'lib', 'main.js')
|
|
32
77
|
const bridgeTarget = join(appDir, 'lib', 'plugin-browser-bridge.js')
|
|
33
78
|
const bridgeSource = join(import.meta.dirname, 'plugin-browser-bridge.js')
|
|
34
79
|
const backupPath = `${mainPath}.before-bridge`
|
|
35
80
|
|
|
81
|
+
// Moving the archive aside breaks two more places that spell `app.asar` out
|
|
82
|
+
// loud, and both are used by `dsh <args>` (the Desktop CLI launcher and the
|
|
83
|
+
// plugin manager behind it), so they are patched here and restored by --revert:
|
|
84
|
+
//
|
|
85
|
+
// runtime/cli/bin/dsh.cmd launches the CLI by that path
|
|
86
|
+
// dsh/.../dsh-desktop-host/lib/cli.js locates `runtime` by that name
|
|
87
|
+
//
|
|
88
|
+
// Without them the CLI dies with MODULE_NOT_FOUND even though the app itself
|
|
89
|
+
// runs fine (the shell resolves everything through app.getAppPath()).
|
|
90
|
+
const cliShimPath = join(resourcesDir, 'runtime', 'cli', 'bin', 'dsh.cmd')
|
|
91
|
+
const cliShimBackup = `${cliShimPath}.before-bridge`
|
|
92
|
+
const hostCliPath = join(appDir, 'dsh', 'node_modules', '@deepseek-ai', 'dsh-desktop-host', 'lib', 'cli.js')
|
|
93
|
+
const hostCliBackup = `${hostCliPath}.before-bridge`
|
|
94
|
+
const CLI_SHIM_MARKER = 'dsh-builtin-browser: resolve the app directory'
|
|
95
|
+
const HOST_CLI_MARKER = 'dsh-builtin-browser: recognize an unpacked `app` directory'
|
|
96
|
+
|
|
36
97
|
/** The exact block appended to main.js. Keep it recognisable: idempotence and
|
|
37
98
|
* reverting both key off this marker. */
|
|
38
99
|
const MARKER = '// dsh-builtin-browser: hand the plugin a way to drive the sidebar'
|
|
@@ -54,25 +115,225 @@ app.whenReady().then(async () => {
|
|
|
54
115
|
});
|
|
55
116
|
`
|
|
56
117
|
|
|
57
|
-
|
|
58
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Parse an asar header — a Chromium pickle holding
|
|
120
|
+
* `[payloadSize][jsonLength][json…]`, preceded by an 8-byte outer pickle.
|
|
121
|
+
* @param file - the archive path.
|
|
122
|
+
* @returns the parsed header tree plus the offset where file content starts.
|
|
123
|
+
*/
|
|
124
|
+
function readAsarHeader(file) {
|
|
125
|
+
const fd = openSync(file, 'r')
|
|
126
|
+
const sizeBuf = Buffer.alloc(8)
|
|
127
|
+
readSync(fd, sizeBuf, 0, 8, 0)
|
|
128
|
+
const headerSize = sizeBuf.readUInt32LE(4)
|
|
129
|
+
const headerBuf = Buffer.alloc(headerSize)
|
|
130
|
+
readSync(fd, headerBuf, 0, headerSize, 8)
|
|
131
|
+
const jsonLength = headerBuf.readUInt32LE(4)
|
|
132
|
+
const header = JSON.parse(headerBuf.toString('utf8', 8, 8 + jsonLength))
|
|
133
|
+
return { fd, header, contentBase: 8 + headerSize }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Write one asar tree to disk, taking `unpacked` entries from the sibling
|
|
138
|
+
* `.unpacked` directory — their bytes are not in the archive.
|
|
139
|
+
* @param asar - the archive path.
|
|
140
|
+
* @param outDir - directory to extract into.
|
|
141
|
+
* @returns file and byte counts, and how many entries came from `.unpacked`.
|
|
142
|
+
*/
|
|
143
|
+
function extractAsar(asar, outDir) {
|
|
144
|
+
const { fd, header, contentBase } = readAsarHeader(asar)
|
|
145
|
+
const unpackedRoot = `${asar}.unpacked`
|
|
146
|
+
let files = 0
|
|
147
|
+
let bytes = 0
|
|
148
|
+
let unpackedFiles = 0
|
|
149
|
+
const walk = (node, prefix) => {
|
|
150
|
+
for (const [name, entry] of Object.entries(node.files ?? {})) {
|
|
151
|
+
const relativePath = prefix === '' ? name : `${prefix}/${name}`
|
|
152
|
+
const target = join(outDir, ...relativePath.split('/'))
|
|
153
|
+
if (entry.files) {
|
|
154
|
+
mkdirSync(target, { recursive: true })
|
|
155
|
+
walk(entry, relativePath)
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
159
|
+
if (entry.unpacked) {
|
|
160
|
+
const source = join(unpackedRoot, ...relativePath.split('/'))
|
|
161
|
+
if (!existsSync(source)) throw new Error(`unpacked entry is missing: ${source}`)
|
|
162
|
+
copyFileSync(source, target)
|
|
163
|
+
unpackedFiles += 1
|
|
164
|
+
} else {
|
|
165
|
+
const buffer = Buffer.alloc(entry.size)
|
|
166
|
+
readSync(fd, buffer, 0, entry.size, contentBase + Number(entry.offset))
|
|
167
|
+
writeFileSync(target, buffer)
|
|
168
|
+
}
|
|
169
|
+
const actual = statSync(target).size
|
|
170
|
+
if (actual !== entry.size) throw new Error(`size mismatch for ${relativePath}: header ${entry.size}, wrote ${actual}`)
|
|
171
|
+
files += 1
|
|
172
|
+
bytes += actual
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
mkdirSync(outDir, { recursive: true })
|
|
176
|
+
walk(header, '')
|
|
177
|
+
return { files, bytes, unpackedFiles }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Make the Desktop CLI launcher resolve the app directory instead of spelling
|
|
182
|
+
* `app.asar` out loud.
|
|
183
|
+
*
|
|
184
|
+
* `resources/runtime/cli/bin/dsh.cmd` is what the `dsh` command on PATH calls,
|
|
185
|
+
* and it launches the CLI through an `app.asar\…` path. Once the archive is
|
|
186
|
+
* moved aside that path is gone, so `dsh <args>` dies with MODULE_NOT_FOUND
|
|
187
|
+
* while the app itself keeps working (the shell resolves everything through
|
|
188
|
+
* `app.getAppPath()`). The patch touches only the app-directory fragment: the
|
|
189
|
+
* launcher keeps its own spelling of everything else, including the executable
|
|
190
|
+
* name, so a build whose binary is named differently still works.
|
|
191
|
+
*
|
|
192
|
+
* @returns what happened, for the install log.
|
|
193
|
+
*/
|
|
194
|
+
function patchCliShim() {
|
|
195
|
+
if (!existsSync(cliShimPath)) return 'launcher not present (skipped)'
|
|
196
|
+
const text = readFileSync(cliShimPath, 'utf8')
|
|
197
|
+
if (text.includes(CLI_SHIM_MARKER)) return 'already patched'
|
|
198
|
+
if (!/%~dp0(?:\.\.\\)+app\.asar\\dsh\\/.test(text)) return 'unexpected launcher shape (skipped)'
|
|
199
|
+
if (!existsSync(cliShimBackup)) copyFileSync(cliShimPath, cliShimBackup)
|
|
200
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n'
|
|
201
|
+
const setLocal = /^.*setlocal.*$/m.exec(text)
|
|
202
|
+
const injection = [
|
|
203
|
+
`rem ${CLI_SHIM_MARKER} (resources\\app when the bridge is installed,`,
|
|
204
|
+
'rem resources\\app.asar on a stock installation).',
|
|
205
|
+
'set "DSH_APP=%~dp0..\\..\\..\\app"',
|
|
206
|
+
'if not exist "%DSH_APP%\\dsh\\node_modules\\@deepseek-ai\\dsh-desktop-host\\lib\\cli.js" set "DSH_APP=%~dp0..\\..\\..\\app.asar"',
|
|
207
|
+
].join(eol)
|
|
208
|
+
const withVariables = setLocal === null
|
|
209
|
+
? `${injection}${eol}${text}`
|
|
210
|
+
: text.replace(setLocal[0], `${setLocal[0]}${eol}${injection}`)
|
|
211
|
+
writeFileSync(cliShimPath, withVariables.replace(/%~dp0(?:\.\.\\)+app\.asar\\dsh\\/, '%DSH_APP%\\dsh\\'))
|
|
212
|
+
return 'patched'
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Teach the Desktop CLI where its `runtime` directory is when the app is an
|
|
217
|
+
* unpacked `app` directory rather than `app.asar`.
|
|
218
|
+
*
|
|
219
|
+
* `dsh-desktop-host/lib/cli.js` derives the directory that holds pnpm as
|
|
220
|
+
* `dirname(runtimeArchivePath(runtimeDir) ?? runtimeDir)`, and
|
|
221
|
+
* `runtimeArchivePath` only recognises a parent literally named `app.asar`.
|
|
222
|
+
* With the app at `resources/app` that falls back to one level too few and
|
|
223
|
+
* resolves `resources/app/runtime`, which does not exist — so the plugin
|
|
224
|
+
* manager behind `dsh plugin …` cannot start.
|
|
225
|
+
*
|
|
226
|
+
* @returns what happened, for the install log.
|
|
227
|
+
*/
|
|
228
|
+
function patchHostCli() {
|
|
229
|
+
if (!existsSync(hostCliPath)) return 'cli.js not present (skipped)'
|
|
230
|
+
const text = readFileSync(hostCliPath, 'utf8')
|
|
231
|
+
if (text.includes(HOST_CLI_MARKER)) return 'already patched'
|
|
232
|
+
const before = '\tawait runDesktopCli(runtimeDir, join(dirname(runtimeArchivePath(runtimeDir) ?? runtimeDir), "runtime"));'
|
|
233
|
+
if (!text.includes(before)) return 'unexpected cli.js shape (skipped)'
|
|
234
|
+
if (!existsSync(hostCliBackup)) copyFileSync(hostCliPath, hostCliBackup)
|
|
235
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n'
|
|
236
|
+
const after = [
|
|
237
|
+
`\t// ${HOST_CLI_MARKER} as the app root,`,
|
|
238
|
+
'\t// so an installation whose app.asar was moved aside still finds its runtime.',
|
|
239
|
+
'\tconst cliOwnerDir = dirname(runtimeDir);',
|
|
240
|
+
'\tconst cliSupportRoot = basename(cliOwnerDir) === "app" || basename(cliOwnerDir) === "app.asar" ? dirname(cliOwnerDir) : cliOwnerDir;',
|
|
241
|
+
'\tawait runDesktopCli(runtimeDir, join(cliSupportRoot, "runtime"));',
|
|
242
|
+
].join(eol)
|
|
243
|
+
writeFileSync(hostCliPath, text.replace(before, after))
|
|
244
|
+
return 'patched'
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Apply both CLI compatibility patches and report them. */
|
|
248
|
+
function installCliCompatibility() {
|
|
249
|
+
console.log(` cli launcher: ${patchCliShim()}`)
|
|
250
|
+
console.log(` cli runtime resolution: ${patchHostCli()}`)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// The archive wins whenever it exists, so ITS presence — not the absence of the
|
|
254
|
+
// directory — decides the layout. After a desktop update the archive is back
|
|
255
|
+
// (a new build) while an older extraction is still lying around, and keying off
|
|
256
|
+
// the directory would patch a tree that Electron never loads.
|
|
257
|
+
const packedLayout = existsSync(asarPath)
|
|
258
|
+
if (!existsSync(mainPath) && !existsSync(asarPath)) {
|
|
259
|
+
console.error('not a DSH Desktop install: neither')
|
|
260
|
+
console.error(` ${mainPath}`)
|
|
261
|
+
console.error('nor')
|
|
262
|
+
console.error(` ${asarPath}`)
|
|
263
|
+
console.error('exists. Pass the installation directory as the first argument.')
|
|
59
264
|
process.exit(1)
|
|
60
265
|
}
|
|
61
266
|
|
|
62
267
|
if (revert) {
|
|
63
|
-
|
|
64
|
-
|
|
268
|
+
let reverted = false
|
|
269
|
+
if (existsSync(asarMoved)) {
|
|
270
|
+
if (existsSync(asarPath)) rmSync(asarPath, { force: true })
|
|
271
|
+
renameSync(asarMoved, asarPath)
|
|
272
|
+
console.log(`restored the app archive: app.asar.before-bridge -> app.asar`)
|
|
273
|
+
reverted = true
|
|
274
|
+
}
|
|
275
|
+
if (existsSync(backupPath)) {
|
|
276
|
+
copyFileSync(backupPath, mainPath)
|
|
277
|
+
console.log(`restored ${mainPath} from ${backupPath}`)
|
|
278
|
+
reverted = true
|
|
279
|
+
}
|
|
280
|
+
if (existsSync(cliShimBackup)) {
|
|
281
|
+
copyFileSync(cliShimBackup, cliShimPath)
|
|
282
|
+
console.log(`restored the CLI launcher: ${cliShimPath}`)
|
|
283
|
+
reverted = true
|
|
284
|
+
}
|
|
285
|
+
if (existsSync(hostCliBackup) && existsSync(dirname(hostCliPath))) {
|
|
286
|
+
copyFileSync(hostCliBackup, hostCliPath)
|
|
287
|
+
console.log(`restored the CLI runtime resolution: ${hostCliPath}`)
|
|
288
|
+
reverted = true
|
|
289
|
+
}
|
|
290
|
+
if (!reverted) {
|
|
291
|
+
console.error(`nothing to revert in ${installRoot}`)
|
|
65
292
|
process.exit(1)
|
|
66
293
|
}
|
|
67
|
-
|
|
68
|
-
console.log(`reverted ${mainPath} from ${backupPath}`)
|
|
294
|
+
console.log(`note: ${appDir} is inert while app.asar exists; delete it to reclaim ~350 MB`)
|
|
69
295
|
process.exit(0)
|
|
70
296
|
}
|
|
71
297
|
|
|
298
|
+
if (packedLayout) {
|
|
299
|
+
const stale = existsSync(appDir)
|
|
300
|
+
const started = Date.now()
|
|
301
|
+
console.log(stale
|
|
302
|
+
? `packed layout detected (${asarPath}); an earlier extraction is present and will be rebuilt`
|
|
303
|
+
: `packed layout detected (${asarPath})`)
|
|
304
|
+
if (stale) {
|
|
305
|
+
// Rebuild rather than merge: the tree has to describe THIS archive, and a
|
|
306
|
+
// merge would keep files the new build no longer ships. Removing it is safe
|
|
307
|
+
// here — the archive is present, so the app is not running from this
|
|
308
|
+
// directory, and a failure below leaves the archive in place and the
|
|
309
|
+
// installation working.
|
|
310
|
+
rmSync(appDir, { recursive: true, force: true })
|
|
311
|
+
}
|
|
312
|
+
console.log(`extracting into ${appDir} — this writes ~355 MB and can take a minute (measured: 8-55s)...`)
|
|
313
|
+
const result = extractAsar(asarPath, appDir)
|
|
314
|
+
const megabytes = (result.bytes / 1024 / 1024).toFixed(1)
|
|
315
|
+
const seconds = ((Date.now() - started) / 1000).toFixed(1)
|
|
316
|
+
console.log(` extracted ${result.files} files / ${megabytes} MB` +
|
|
317
|
+
` (${result.unpackedFiles} from app.asar.unpacked) in ${seconds}s`)
|
|
318
|
+
// A fresh extraction invalidates any earlier pristine copy: --revert has to be
|
|
319
|
+
// able to restore the main.js that belongs to THIS build.
|
|
320
|
+
copyFileSync(mainPath, backupPath)
|
|
321
|
+
try {
|
|
322
|
+
renameSync(asarPath, asarMoved)
|
|
323
|
+
console.log('moved the archive aside: app.asar -> app.asar.before-bridge')
|
|
324
|
+
console.log(` Electron now loads ${appDir}; the archive is untouched, so the asar integrity fuse cannot trip`)
|
|
325
|
+
} catch (error) {
|
|
326
|
+
console.error(`could not move app.asar aside: ${String(error)}`)
|
|
327
|
+
console.error('close DSH Desktop completely and run this again.')
|
|
328
|
+
process.exit(1)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
72
332
|
const current = readFileSync(mainPath, 'utf8')
|
|
73
333
|
if (current.includes(MARKER)) {
|
|
74
334
|
console.log('bridge already installed (main.js carries the marker); refreshing the module copy')
|
|
75
335
|
copyFileSync(bridgeSource, bridgeTarget)
|
|
336
|
+
installCliCompatibility()
|
|
76
337
|
process.exit(0)
|
|
77
338
|
}
|
|
78
339
|
|
|
@@ -95,4 +356,5 @@ copyFileSync(bridgeSource, bridgeTarget)
|
|
|
95
356
|
console.log(`installed bridge into ${appDir}`)
|
|
96
357
|
console.log(` module: ${bridgeTarget}`)
|
|
97
358
|
console.log(` patch: ${mainPath} (+${SNIPPET.split('\n').length} lines)`)
|
|
359
|
+
installCliCompatibility()
|
|
98
360
|
console.log('restart DSH Desktop to activate it.')
|
|
@@ -24,12 +24,24 @@
|
|
|
24
24
|
import { createServer } from 'node:net'
|
|
25
25
|
import { randomBytes } from 'node:crypto'
|
|
26
26
|
import { writeFileSync, mkdirSync } from 'node:fs'
|
|
27
|
+
import { homedir } from 'node:os'
|
|
27
28
|
import { join } from 'node:path'
|
|
28
29
|
import { app, webContents } from 'electron'
|
|
29
30
|
|
|
30
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Absolute path of the endpoint file the plugin reads.
|
|
33
|
+
*
|
|
34
|
+
* The fallback has to match the plugin's own (`process.env.DSH_HOME ??
|
|
35
|
+
* homedir()/.dsh`, see src/browser-electron/desktop-bridge-host.ts). With a
|
|
36
|
+
* bare `?? ''` an unset DSH_HOME resolves to the process CWD, so the endpoint
|
|
37
|
+
* landed beside the app — while the plugin looked in ~/.dsh and never found it.
|
|
38
|
+
* The failure is silent in both directions: discovery returns undefined and the
|
|
39
|
+
* plugin quietly falls back to a self-hosted window, with no error anywhere.
|
|
40
|
+
* Measured on DSH Desktop 0.2.0-rc.2, launched from a shortcut (no DSH_HOME in
|
|
41
|
+
* its environment).
|
|
42
|
+
*/
|
|
31
43
|
function endpointFile() {
|
|
32
|
-
const home = process.env.DSH_HOME ?? ''
|
|
44
|
+
const home = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
33
45
|
return join(home, 'dsh-builtin-browser-bridge.json')
|
|
34
46
|
}
|
|
35
47
|
|
|
@@ -128,10 +140,21 @@ async function handle(request) {
|
|
|
128
140
|
// b. otherwise type into the address field and submit with REAL key input
|
|
129
141
|
// (synthetic events do not move React's controlled input, and the field
|
|
130
142
|
// also loses focus to re-renders, so both halves are needed).
|
|
143
|
+
let submitVerdict = 'n/a'
|
|
131
144
|
const restored = await sendCdp(shell.id, 'Runtime.evaluate', {
|
|
145
|
+
// Only real controls count, and only their OWN text does. Matching any
|
|
146
|
+
// node's `textContent` made a container that merely mentioned restoring a
|
|
147
|
+
// page report RESTORED — which skipped the address route below entirely
|
|
148
|
+
// and left the guest unmaterialized (measured on DSH 0.2.0-rc.2, where the
|
|
149
|
+
// sidebar shows no restore control at all).
|
|
132
150
|
expression: `(() => {
|
|
133
151
|
const nodes = Array.from(document.querySelectorAll('button,a,[role=button],div[role=link]'));
|
|
134
|
-
const hit = nodes.find(n =>
|
|
152
|
+
const hit = nodes.find(n => {
|
|
153
|
+
const own = (n.textContent || '').trim();
|
|
154
|
+
if (own.length > 24) return false;
|
|
155
|
+
return /^(恢复页面|恢复上次|恢复上次页面|上次打开)$/.test(own)
|
|
156
|
+
|| /恢复|restore/i.test(n.getAttribute('aria-label') || n.getAttribute('title') || '');
|
|
157
|
+
});
|
|
135
158
|
if (hit === undefined) return 'NO_RESTORE';
|
|
136
159
|
hit.click();
|
|
137
160
|
return 'RESTORED';
|
|
@@ -140,8 +163,15 @@ async function handle(request) {
|
|
|
140
163
|
})
|
|
141
164
|
if (restored?.result?.value !== 'RESTORED') {
|
|
142
165
|
if (url !== '') {
|
|
143
|
-
// Set the value the way React accepts it, then
|
|
144
|
-
|
|
166
|
+
// Set the value the way React accepts it, then SUBMIT THE FORM.
|
|
167
|
+
//
|
|
168
|
+
// A dispatched Enter is not enough: the sidebar browser's toolbar is a
|
|
169
|
+
// `<form>` whose submission is wired to an explicit control (aria-label
|
|
170
|
+
// "前往" / "Go"), and implicit submission does not fire there — measured
|
|
171
|
+
// on DSH 0.2.0-rc.2, where Enter left the field filled and the guest
|
|
172
|
+
// uncreated. `requestSubmit()` goes through the same path the button
|
|
173
|
+
// does, so it works with React's onSubmit and needs no localized label.
|
|
174
|
+
const typed = await sendCdp(shell.id, 'Runtime.evaluate', {
|
|
145
175
|
expression: `(() => {
|
|
146
176
|
const inputs = Array.from(document.querySelectorAll('input')).filter(i => /HTTP|地址|url/i.test((i.placeholder || '') + (i.getAttribute('aria-label') || '')));
|
|
147
177
|
if (inputs.length === 0) return 'NO_ADDRESS_BAR';
|
|
@@ -150,18 +180,28 @@ async function handle(request) {
|
|
|
150
180
|
setter.call(input, ${JSON.stringify(String(request.url ?? ''))});
|
|
151
181
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
152
182
|
input.focus();
|
|
153
|
-
|
|
183
|
+
const form = input.form ?? input.closest('form');
|
|
184
|
+
if (form) { form.requestSubmit(); return 'SUBMITTED_FORM'; }
|
|
185
|
+
const button = input.parentElement
|
|
186
|
+
? input.parentElement.querySelector('button[type=submit],button')
|
|
187
|
+
: null;
|
|
188
|
+
if (button) { button.click(); return 'CLICKED_SUBMIT'; }
|
|
189
|
+
return 'TYPED_ONLY';
|
|
154
190
|
})()`,
|
|
155
191
|
returnByValue: true,
|
|
156
192
|
})
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
193
|
+
submitVerdict = String(typed?.result?.value ?? 'n/a')
|
|
194
|
+
// Enter stays as the last resort for sidebars without a form.
|
|
195
|
+
if (submitVerdict === 'TYPED_ONLY') {
|
|
196
|
+
for (const type of ['keyDown', 'keyUp']) {
|
|
197
|
+
await sendCdp(shell.id, 'Input.dispatchKeyEvent', {
|
|
198
|
+
type,
|
|
199
|
+
key: 'Enter',
|
|
200
|
+
code: 'Enter',
|
|
201
|
+
windowsVirtualKeyCode: 13,
|
|
202
|
+
nativeVirtualKeyCode: 13,
|
|
203
|
+
})
|
|
204
|
+
}
|
|
165
205
|
}
|
|
166
206
|
}
|
|
167
207
|
}
|
|
@@ -177,7 +217,7 @@ async function handle(request) {
|
|
|
177
217
|
return { ok: true, created: true, id: created[0].id, via: restored?.result?.value === 'RESTORED' ? 'restore' : 'address' }
|
|
178
218
|
}
|
|
179
219
|
}
|
|
180
|
-
throw new Error(`the sidebar did not create a browser guest (prepare=${String(verdict)}, restore=${String(restored?.result?.value)})`)
|
|
220
|
+
throw new Error(`the sidebar did not create a browser guest (prepare=${String(verdict)}, restore=${String(restored?.result?.value)}, submit=${submitVerdict})`)
|
|
181
221
|
}
|
|
182
222
|
if (op === 'ensureTabs') {
|
|
183
223
|
// Grow the sidebar's browser tab strip to `count` tabs and report every guest.
|
package/docs/architecture.md
CHANGED
|
@@ -61,7 +61,7 @@ RemoteElectronViewHost ──TCP JSON-RPC──▶ host-main.js
|
|
|
61
61
|
- **协议**:本机 loopback TCP,每行一个 JSON(`{ id, op, ... }` ↔ `{ id, ok, result|err }`);
|
|
62
62
|
- **认证**:每次 spawn 生成随机 token,经 **stdin 首行 + `DSH_BROWSER_RPC_TOKEN` 环境变量双通道**传给子进程(绝不进 argv,避免进程列表泄露;Windows GUI 进程收不到 piped stdin,环境变量兜底),子进程首条消息必须回传该 token(`hello`);服务端只接受**一个**连接,其余连接直接断开——本机其他进程无法伪冒子进程或注入回复;
|
|
63
63
|
- **Electron 定位**:① `ELECTRON_PATH`(显式覆盖,优先于一切自动发现)→ ② 随插件安装的 electron 包(纯文件系统探测,含 pnpm store 布局,不触发 44+ 懒下载)→ ③ 锚点与 pnpm store 中**版本最新**者(33.x 有合成器缺陷,建议 ≥ 40)→ ④ 当前进程是**裸** Electron(dev 模式)时复用宿主二进制(`process.execPath`)→ ⑤ 进程祖先树中的裸 Electron 二进制(Windows 用 PowerShell CIM 查询,仅在其它路径全部落空时才执行)。**打包应用不参与复用**:旁有 `resources/app.asar` 的可执行文件(如 DSH Desktop.exe)不能按脚本参数拉起——spawn 会启动应用本体并秒退(单实例锁,即 "browser host exited (code=0)"),一律跳过;
|
|
64
|
-
- **稳健性**:子进程/套接字都有 `error` 监听(否则未捕获事件会炸掉整个 DSH 进程);子进程退出自动重启,已物化的视图句柄在下次调用时**自动重建并重试一次**(会话跨崩溃存活,仅页面状态丢失,不再出现 "browser host is not running" 僵尸态);物化失败可重试;下载仅限 HTTP(S)、`savePath` 必须绝对路径(
|
|
64
|
+
- **稳健性**:子进程/套接字都有 `error` 监听(否则未捕获事件会炸掉整个 DSH 进程);子进程退出自动重启,已物化的视图句柄在下次调用时**自动重建并重试一次**(会话跨崩溃存活,仅页面状态丢失,不再出现 "browser host is not running" 僵尸态);物化失败可重试;下载仅限 HTTP(S)、`savePath` 必须绝对路径(`downloadDir` 已配置时用它;未配置时按序探测 —— 存在的 `XDG_DOWNLOAD_DIR` → home 下第一个存在的 `Downloads`/`下载`/`下載` → 回退 `~/Downloads`,首次使用时创建)、流式限流 + Content-Length 提前拒绝、256MB 上限与 60s 超时,文件由**子进程直接落盘**(临时文件 + rename,不再经 RPC 传 base64);cookie 导出/恢复有 30s 超时,且两者都受设置里 `credentials.allowRead` 门控 —— 关闭时抛 `BROWSER_AUTH_DISABLED`(导出**与恢复**都拒绝),有原生方法时优先用它,否则走 CDP 的 `Storage.getCookies` / `Storage.setCookies`(三种载体都可用);
|
|
65
65
|
- **视图可见性**:多标签/多会话时,`showView` 隐藏其他视图并把目标视图置顶(remove+re-add),确保用户看到的是活动标签;目标是当前可见视图时**跳过** remove/re-add(否则每次操作都闪烁);窗口标题实时显示当前可见会话的任务标识 + 页面标题/URL(`showView` 携带会话 label,子进程读 `getTitle`/`getURL`);窗口 resize 时视图 bounds 自动跟随;
|
|
66
66
|
- **孤儿防护**:父进程断开时子进程自动退出,不留僵尸窗口;
|
|
67
67
|
- **cookie 落盘**:子进程使用独立 userData 目录(`<DSH_HOME>/dsh-builtin-browser-host`),登录态跨重启保留(另有 `browser_auth` 手动导出/恢复)。
|
package/docs/tool-reference.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 工具参考
|
|
2
2
|
|
|
3
|
-
全部 34 个 `browser_*` 工具。守卫列:✅ 表示该动作受 `browser_restrict`
|
|
3
|
+
全部 34 个 `browser_*` 工具。守卫列:✅ 表示该动作受 `browser_restrict` 白名单约束;**「– 豁免」表示无论白名单怎么写都不拦截**(豁免集合为 `READ_ONLY_TOOLS`,`src/tool-browser/index.ts` 导出:**只观察的工具** —— `snapshot`/`a11y`/`content`/`scrape`/`screenshot`/`get_value`/`wait`/`challenge`/`list_tabs`/`session`/`history`/`visited`/`auth` —— 加上 **解除限制 / 从坏状态恢复的工具** `restrict`/`reset_session`/`reset`;`restrict` 在其中,否则限制到空的任务再也解不开);**「–」表示该工具只观察、不做动作**,故没有可拦的东西。两者之外的工具都受白名单约束 —— 例如 `browser_close_tab` 不属于豁免集合。
|
|
4
4
|
|
|
5
5
|
## 页面与导航
|
|
6
6
|
|
|
@@ -30,16 +30,16 @@
|
|
|
30
30
|
| `browser_check` | `target`(必填), `checked?` | `{ checked }` | ✅ | 勾选/取消勾选 checkbox 或 radio(按 target 定位) |
|
|
31
31
|
| `browser_select` | `target`(必填), `optionValue?`/`optionText?`/`optionIndex?`(三选一) | `{ value, text }` | ✅ | 选中 `<select>` 的某个选项(按 target 定位) |
|
|
32
32
|
| `browser_clear` | `target`(必填) | `{ cleared }` | ✅ | 清空输入/文本域/contenteditable,或取消勾选 checkbox/radio |
|
|
33
|
-
| `browser_get_value` | `target`(必填) | `{ value?, checked?, selectedText? }` | – | 读取元素当前值,用于操作后验证 |
|
|
34
|
-
| `browser_scrape` | `item`(必填), `fields`(必填,映射), `timeoutMs?` | `{ count, items[] }` | – | 结构化提取:容器选择器 + 字段映射(如 `{"title": "h3", "url": "a@href"}`);静态 CSS 查询、不执行任意代码、CSP 安全;等待 item 出现(默认 5s) |
|
|
33
|
+
| `browser_get_value` | `target`(必填) | `{ value?, checked?, selectedText? }` | – 豁免 | 读取元素当前值,用于操作后验证 |
|
|
34
|
+
| `browser_scrape` | `item`(必填), `fields`(必填,映射), `timeoutMs?` | `{ count, items[] }` | – 豁免 | 结构化提取:容器选择器 + 字段映射(如 `{"title": "h3", "url": "a@href"}`);静态 CSS 查询、不执行任意代码、CSP 安全;等待 item 出现(默认 5s) |
|
|
35
35
|
|
|
36
36
|
## 标签与会话
|
|
37
37
|
|
|
38
38
|
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
39
39
|
| --- | --- | --- | --- | --- |
|
|
40
|
-
| `browser_list_tabs` | – | `{
|
|
40
|
+
| `browser_list_tabs` | – | `{ tabs: [{ id, url, active }] }` | – | 当前会话的标签列表(输出 schema 只有 `tabs`,**没有 `session` 字段**;会话标识请用 `browser_session`) |
|
|
41
41
|
| `browser_switch_tab` | `tabId`(必填) | `{ switched }` | ✅ | 按 id 切换标签;自托管下同步切换可见视图 |
|
|
42
|
-
| `browser_close_tab` | `tabId`(必填) | `{ closed }` |
|
|
42
|
+
| `browser_close_tab` | `tabId`(必填) | `{ closed }` | ✅ | 关闭标签;关闭活动标签后激活下一个。**不属于只读豁免集合,受白名单约束**;`tabId` 只在本会话内查找(接受 `tab:<uuid>` 或裸 uuid),陈旧 id 不会关掉别的任务的标签页 |
|
|
43
43
|
| `browser_reset` | – | `{ reset }` | ✅ | 关闭本任务所有标签,回到一个空白标签 |
|
|
44
44
|
| `browser_session` | – | `{ session, tabs[] }` | – | 查看本任务的浏览器会话与标签 |
|
|
45
45
|
| `browser_reset_session` | – | `{ reset }` | ✅ | 关闭并重建本任务的浏览器会话(崩溃/卡死后恢复) |
|
|
@@ -57,14 +57,14 @@
|
|
|
57
57
|
|
|
58
58
|
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
59
59
|
| --- | --- | --- | --- | --- |
|
|
60
|
-
| `browser_auth` | `action`(flush/restore,必填), `cookies?` | `{ cookies[]? / restored? }` | ✅ | 导出/恢复 cookie
|
|
61
|
-
| `browser_restrict` | `allowed?` | `{ restrictedTo[] }` | – |
|
|
60
|
+
| `browser_auth` | `action`(flush/restore,必填), `cookies?` | `{ cookies[]? / restored? }` | ✅ | 导出/恢复 cookie —— **三种载体都可用**:自托管走原生会话,桌面侧栏与本机 Chrome/Edge 走 CDP 的 `Storage.getCookies` / `Storage.setCookies`;flush 返回 cookie 列表,restore 带列表写回。设置里「允许读取 cookies / 导出登录状态」关闭时,flush 与 restore **都**抛 `BROWSER_AUTH_DISABLED` |
|
|
61
|
+
| `browser_restrict` | `allowed?` | `{ restrictedTo[] }` | – | 设置动作白名单;空列表解除;**只校验名字是否以 `browser_` 开头**(写错的前缀会报错,`browser_typo` 这种拼错但前缀合法的名字会被接受、等同于拦掉该名字,不额外报错);守卫按名字匹配,因此白名单里列什么名字就只放行什么名字。**软护栏**——模型可自行解除,非安全边界 |
|
|
62
62
|
|
|
63
63
|
## 截图
|
|
64
64
|
|
|
65
65
|
| 工具 | 参数 | 输出 | 守卫 | 说明 |
|
|
66
66
|
| --- | --- | --- | --- | --- |
|
|
67
|
-
| `browser_screenshot` | `fullPage?`, `savePath?`, `format?`(png/jpeg), `quality?`, `maxWidth?`, `maxHeight?` | `{ dataUrl, path? }` | – | 截图;PNG
|
|
67
|
+
| `browser_screenshot` | `fullPage?`, `savePath?`, `format?`(png/jpeg), `quality?`, `maxWidth?`, `maxHeight?` | `{ dataUrl, path? }` | – | 截图;PNG 默认,**JPEG 在自托管与本机 Chrome/Edge 上都可用**(自托管在原生 `capturePage` 路径编码,本机浏览器经 CDP 声明 `supportsCdpJpeg`;**桌面侧栏**的 Electron CDP JPEG 编码器会挂起,该载体下 JPEG 请求返回 PNG);`maxWidth`/`maxHeight` 等比缩放**三种载体都支持**;`fullPage` 会跳过原生 `capturePage`、三种载体统一走 CDP `captureBeyondViewport`;`savePath` 落盘供视觉模型读取(与 `browser_download` 同一准入门:必须位于 `downloadDir` 内且不覆盖已有文件) |
|
|
68
68
|
|
|
69
69
|
## 常用组合
|
|
70
70
|
|
package/docs/user-guide.md
CHANGED
|
@@ -17,10 +17,12 @@ dsh plugin --profile web add dsh-builtin-browser
|
|
|
17
17
|
dsh plugin --profile web add <本仓库路径>
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
安装会链接插件、把 `dsh-builtin-browser` 加入 profile 的 bundle
|
|
20
|
+
安装会链接插件、把 `dsh-builtin-browser` 加入 profile 的 bundle 层,并挂载四行(一个惰性的根行 + 三个功能行):
|
|
21
21
|
|
|
22
22
|
| 行 | 子路径 | 角色 |
|
|
23
23
|
| --- | --- | --- |
|
|
24
|
+
| `dsh-builtin-browser` | 包名本身 | **惰性根行**:不注册任何东西(空 `apply()`),只用来声明包名 —— 宿主的客户端插件扫描靠精确包名读到 `dsh.client`,少了这行设置栏不会出现 |
|
|
25
|
+
| --- | --- | --- |
|
|
24
26
|
| `browser` | `dsh-builtin-browser/browser` | `ctx.browser` 能力 seam(始终挂载) |
|
|
25
27
|
| `browser-electron` | `dsh-builtin-browser/browser-electron` | Electron CDP provider |
|
|
26
28
|
| `tool-browser` | `dsh-builtin-browser/tool-browser` | `browser_*` 模型侧工具 |
|
|
@@ -31,7 +33,7 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
31
33
|
|
|
32
34
|
| 行 | 配置项 | 类型 | 默认 | 说明 |
|
|
33
35
|
| --- | --- | --- | --- | --- |
|
|
34
|
-
| `browser-electron` | `viewHost` | 对象 |
|
|
36
|
+
| `browser-electron` | `viewHost` | 对象 | 可选 | 宿主提供的 `ElectronBrowserViewHost`(通常 `!!js ctx.get('electronViewHost')`)。**不传时插件自己选载体**:桌面端驱动官方侧栏、否则自托管;设置里显式选择的 Chrome/Edge 优先于两者 |
|
|
35
37
|
| `browser-electron` | `httpOnly` | 布尔 | `true` | 仅允许 HTTP(S) 导航;`file:`/`data:` 等拒绝 |
|
|
36
38
|
| `browser-electron` | `snapshotMaxElements` | 数字 | `60` | 快照最多收录的交互元素数 |
|
|
37
39
|
| `browser-electron` | `contentMaxChars` | 数字 | `100000` | 内容抓取默认字符上限 |
|
package/docs/why-browser.md
CHANGED
|
@@ -33,7 +33,7 @@ LLM agent 需要"上网"。传统做法是给 agent 一个**无头浏览器**(Pl
|
|
|
33
33
|
## 设计取舍
|
|
34
34
|
|
|
35
35
|
- **登录态共享**:所有任务共享同一份 cookie(符合"用户登录一次,agent 到处可用");需要隔离时用 `browser_auth` 手动导出/恢复。
|
|
36
|
-
- **任务级会话隔离**:每个 DSH 会话(任务)拥有独立的浏览器会话(标签页与历史)
|
|
36
|
+
- **任务级会话隔离**:每个 DSH 会话(任务)拥有独立的浏览器会话(标签页与历史),并发任务互不抢页面;自托管载体下**每个会话连窗口都各自一份**(窗口按会话 id 分组,窗口标题带任务标识),侧栏与本机浏览器载体则是同一个宿主里切换当前可见视图。
|
|
37
37
|
- **装好即用**:不依赖桌面外壳。有外壳时嵌入外壳视图;纯 `dsh web` 时插件自托管——自己拉起 Electron 窗口,通过本机 TCP JSON-RPC 驱动。
|
|
38
38
|
- **边界清晰**:可见视图、浏览器列布局属于宿主外壳;插件只负责 seam、provider 与工具,不含任何 UI。
|
|
39
39
|
- **人机验证不硬刚**:检测到 Cloudflare/reCAPTCHA/hCaptcha/Turnstile 时停下,请用户在共享窗口人工完成,而不是盲目重试。
|
|
@@ -35,7 +35,6 @@ export declare function bridgeEndpointPath(): string;
|
|
|
35
35
|
* point is a single page both parties can see.
|
|
36
36
|
*/
|
|
37
37
|
export declare class DesktopBridgeViewHost implements ElectronBrowserViewHost {
|
|
38
|
-
private readonly endpoint;
|
|
39
38
|
private readonly connection;
|
|
40
39
|
private readonly views;
|
|
41
40
|
/**
|
|
@@ -47,6 +46,16 @@ export declare class DesktopBridgeViewHost implements ElectronBrowserViewHost {
|
|
|
47
46
|
* session gets its own page).
|
|
48
47
|
*/
|
|
49
48
|
private readonly orphaned;
|
|
49
|
+
/**
|
|
50
|
+
* Registered by the provider, and deliberately never invoked on this carrier.
|
|
51
|
+
*
|
|
52
|
+
* The sidebar is the shell's own interface: a human clicking inside that page is an
|
|
53
|
+
* event the shell owns, and the bridge exposes no operation that reports it. The
|
|
54
|
+
* handler is stored anyway so the interface contract holds (a caller may register
|
|
55
|
+
* one and must not be surprised) and so a future bridge operation can deliver it
|
|
56
|
+
* without changing the provider. Consequently no user-action event is emitted while
|
|
57
|
+
* the desktop sidebar is the carrier — by design, not by omission.
|
|
58
|
+
*/
|
|
50
59
|
private userActionHandler;
|
|
51
60
|
/**
|
|
52
61
|
* @param endpoint - the shell's published bridge endpoint.
|
|
@@ -50,7 +50,6 @@ function isGuestGone(error) {
|
|
|
50
50
|
* point is a single page both parties can see.
|
|
51
51
|
*/
|
|
52
52
|
export class DesktopBridgeViewHost {
|
|
53
|
-
endpoint;
|
|
54
53
|
connection;
|
|
55
54
|
views = new Map();
|
|
56
55
|
/**
|
|
@@ -62,12 +61,21 @@ export class DesktopBridgeViewHost {
|
|
|
62
61
|
* session gets its own page).
|
|
63
62
|
*/
|
|
64
63
|
orphaned = new Set();
|
|
64
|
+
/**
|
|
65
|
+
* Registered by the provider, and deliberately never invoked on this carrier.
|
|
66
|
+
*
|
|
67
|
+
* The sidebar is the shell's own interface: a human clicking inside that page is an
|
|
68
|
+
* event the shell owns, and the bridge exposes no operation that reports it. The
|
|
69
|
+
* handler is stored anyway so the interface contract holds (a caller may register
|
|
70
|
+
* one and must not be surprised) and so a future bridge operation can deliver it
|
|
71
|
+
* without changing the provider. Consequently no user-action event is emitted while
|
|
72
|
+
* the desktop sidebar is the carrier — by design, not by omission.
|
|
73
|
+
*/
|
|
65
74
|
userActionHandler;
|
|
66
75
|
/**
|
|
67
76
|
* @param endpoint - the shell's published bridge endpoint.
|
|
68
77
|
*/
|
|
69
78
|
constructor(endpoint) {
|
|
70
|
-
this.endpoint = endpoint;
|
|
71
79
|
this.connection = new BridgeConnection(endpoint);
|
|
72
80
|
}
|
|
73
81
|
/**
|