dsh-zen-remote 2.2.7 → 2.2.9
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/docs/interface.md +6 -4
- package/docs/remote-access.md +1 -0
- package/lib/client-routes.js +52 -0
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +506 -280
- package/lib/relay-client.js +71 -0
- package/lib/relay-client.js.map +1 -1
- package/lib/types/client/MobileAttachButton.d.ts +15 -7
- package/lib/types/client/MobileAttachButton.d.ts.map +1 -1
- package/lib/types/client/MobileAttachChips.d.ts.map +1 -1
- package/lib/types/client/effects/model-sheet-extras.d.ts +29 -25
- package/lib/types/client/effects/model-sheet-extras.d.ts.map +1 -1
- package/lib/types/client/styles/composer.css.d.ts +1 -1
- package/lib/types/client/styles/composer.css.d.ts.map +1 -1
- package/lib/types/client-data/remote-status.d.ts +39 -2
- package/lib/types/client-data/remote-status.d.ts.map +1 -1
- package/lib/types/client-routes.d.ts +19 -0
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/relay-client.d.ts +10 -0
- package/lib/types/relay-client.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/check-attach-upload.mjs +42 -0
- package/src/client/MobileAttachButton.tsx +23 -12
- package/src/client/MobileAttachChips.tsx +0 -7
- package/src/client/effects/model-sheet-extras.ts +233 -111
- package/src/client/styles/composer.css.ts +142 -83
- package/src/client-data/remote-status.ts +128 -2
- package/src/client-routes.ts +53 -0
- package/src/relay-client.ts +76 -0
- package/lib/types/client/host-attach.d.ts +0 -38
- package/lib/types/client/host-attach.d.ts.map +0 -1
- package/src/client/host-attach.ts +0 -54
package/src/relay-client.ts
CHANGED
|
@@ -57,6 +57,9 @@ const RELAY_PROTOCOL = 1
|
|
|
57
57
|
const HANDSHAKE_PATH = '/_dsh/zen-remote/relay/v1/handshake'
|
|
58
58
|
const INVOKE_PATH = '/_dsh/zen-remote/relay/v1/invoke'
|
|
59
59
|
const STREAM_PATH = '/_dsh/zen-remote/relay/v1/stream'
|
|
60
|
+
/** the relay's existing authenticated health route
|
|
61
|
+
* (`GET …/relay/ping`, answers `{ok:true}`). Used by probeHealth() only. */
|
|
62
|
+
const PING_PATH = '/_dsh/zen-remote/relay/ping'
|
|
60
63
|
const UNSHARE_PATH = '/_dsh/zen-remote/relay/v1/unshare'
|
|
61
64
|
const EVENT_RESULT_PATH = '/_dsh/zen-remote/relay/v1/event-result'
|
|
62
65
|
const HTTP_PATH = '/_dsh/zen-remote/relay/v1/http'
|
|
@@ -318,6 +321,18 @@ export interface RelayClient {
|
|
|
318
321
|
* ladder (the settings page's 立即重连). Answers whether an attempt was
|
|
319
322
|
* started — the reconnect route turns a false into a 409. */
|
|
320
323
|
reconnect(): boolean
|
|
324
|
+
|
|
325
|
+
/** ONE lightweight authenticated health probe
|
|
326
|
+
* (`GET /ping`). Never changes `state`; de-duplicated while in flight;
|
|
327
|
+
* resolves false on any refusal. NOT a poller — call it on a real signal. */
|
|
328
|
+
probeHealth(): Promise<boolean>
|
|
329
|
+
|
|
330
|
+
/** a CONTROLLED fast-path for a reliable
|
|
331
|
+
* "network is back" signal. Fires only from `offline`, refuses within
|
|
332
|
+
* NETWORK_HINT_MIN_GAP_MS of the previous hint (no storm), and otherwise
|
|
333
|
+
* behaves exactly like reconnect(). connect() de-duplicates concurrent
|
|
334
|
+
* attempts via connectInFlight. Answers whether an attempt started. */
|
|
335
|
+
networkRestored(): boolean
|
|
321
336
|
/** Notify that the credentials source may have changed (the loader's
|
|
322
337
|
* volatile-update for the row): a pending reconnect wait is cancelled and,
|
|
323
338
|
* with credentials present, one immediate attempt runs with the new
|
|
@@ -1308,6 +1323,62 @@ export function createRelayClient(options: CreateRelayClientOptions): RelayClien
|
|
|
1308
1323
|
return true
|
|
1309
1324
|
}
|
|
1310
1325
|
|
|
1326
|
+
/**
|
|
1327
|
+
* one authenticated `GET /ping` — the independent
|
|
1328
|
+
* health confirmation a restored link needs when no business traffic is
|
|
1329
|
+
* flowing. Never throws, never moves `state`, de-duplicated in flight.
|
|
1330
|
+
*/
|
|
1331
|
+
let healthProbeInFlight: Promise<boolean> | undefined
|
|
1332
|
+
async function probeHealth(): Promise<boolean> {
|
|
1333
|
+
if (healthProbeInFlight !== undefined) return healthProbeInFlight
|
|
1334
|
+
const attempt = (async (): Promise<boolean> => {
|
|
1335
|
+
let creds: { url: string, token: string }
|
|
1336
|
+
try {
|
|
1337
|
+
creds = requireCredentials()
|
|
1338
|
+
} catch {
|
|
1339
|
+
return false
|
|
1340
|
+
}
|
|
1341
|
+
const ac = new AbortController()
|
|
1342
|
+
const timer = setTimeout(() => ac.abort(), Math.min(requestTimeoutMs, 8_000))
|
|
1343
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
1344
|
+
try {
|
|
1345
|
+
const response = await fetchImpl(`${creds.url}${PING_PATH}`, {
|
|
1346
|
+
method: 'GET',
|
|
1347
|
+
headers: {
|
|
1348
|
+
authorization: `Bearer ${creds.token}`,
|
|
1349
|
+
accept: 'application/json',
|
|
1350
|
+
},
|
|
1351
|
+
redirect: 'manual',
|
|
1352
|
+
signal: ac.signal,
|
|
1353
|
+
})
|
|
1354
|
+
return response.ok
|
|
1355
|
+
} catch {
|
|
1356
|
+
return false
|
|
1357
|
+
} finally {
|
|
1358
|
+
clearTimeout(timer)
|
|
1359
|
+
}
|
|
1360
|
+
})()
|
|
1361
|
+
healthProbeInFlight = attempt
|
|
1362
|
+
try {
|
|
1363
|
+
return await attempt
|
|
1364
|
+
} finally {
|
|
1365
|
+
if (healthProbeInFlight === attempt) healthProbeInFlight = undefined
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/** controlled fast-path; see the interface note. */
|
|
1370
|
+
const NETWORK_HINT_MIN_GAP_MS = 3_000
|
|
1371
|
+
let lastNetworkHintAt = 0
|
|
1372
|
+
function networkRestored(): boolean {
|
|
1373
|
+
if (stopped || state !== 'offline') return false
|
|
1374
|
+
const now = clock.now()
|
|
1375
|
+
if (now - lastNetworkHintAt < NETWORK_HINT_MIN_GAP_MS) return false
|
|
1376
|
+
lastNetworkHintAt = now
|
|
1377
|
+
cancelRetry()
|
|
1378
|
+
void connect().catch(() => {})
|
|
1379
|
+
return true
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1311
1382
|
/**
|
|
1312
1383
|
* The credentials source moved (a pairing write, an unpair, a hand edit
|
|
1313
1384
|
* committed by the loader): drop any pending wait — the ladder belongs to
|
|
@@ -1907,6 +1978,11 @@ export function createRelayClient(options: CreateRelayClientOptions): RelayClien
|
|
|
1907
1978
|
queueDeviceName,
|
|
1908
1979
|
connect,
|
|
1909
1980
|
reconnect,
|
|
1981
|
+
// the two new surfaces. `probeHealth` is the
|
|
1982
|
+
// authenticated GET /ping health check (never moves `state`);
|
|
1983
|
+
// `networkRestored` is the controlled fast-path (offline-only + 3s gap).
|
|
1984
|
+
probeHealth,
|
|
1985
|
+
networkRestored,
|
|
1910
1986
|
credentialsChanged,
|
|
1911
1987
|
stop,
|
|
1912
1988
|
invoke,
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The host composer's own hidden file picker, when the running DSH ships one.
|
|
3
|
-
*
|
|
4
|
-
* DSH 0.1.5 grew a native attachment flow (dsh-attachment / dsh-client-file-
|
|
5
|
-
* upload): the official tool row carries a second `_add` paperclip whose click
|
|
6
|
-
* drives an `<input type="file" hidden>` that is a DIRECT sibling of the
|
|
7
|
-
* buttons inside `_tools`. That input is the one structural, locale-free
|
|
8
|
-
* proof that the host implements composer attachment upload — the button
|
|
9
|
-
* itself can only be told apart from the "+" command menu by its localized
|
|
10
|
-
* aria-label, and the capability prop (`addFiles`) lives on the composer
|
|
11
|
-
* bar's own inject face, which never reaches `conversation.input.left`
|
|
12
|
-
* registrants. Scoped to `_tools` children so a third-party plugin parking
|
|
13
|
-
* its own file input somewhere else in the composer cannot be misread as
|
|
14
|
-
* the official feature; `:not([data-mobile-nav])` keeps OUR picker
|
|
15
|
-
* (`attach-picker`) out of the match.
|
|
16
|
-
*/
|
|
17
|
-
export declare const HOST_ATTACH_INPUT = "[data-slot=\"conversation.composer.bar\"] [class$=\"_tools\"] > input[type=\"file\"]:not([data-mobile-nav])";
|
|
18
|
-
/** Synchronous read: does this host's composer ship its own attach picker? */
|
|
19
|
-
export declare function hostAttachPresent(): boolean;
|
|
20
|
-
/**
|
|
21
|
-
* True while OUR attachment UI should mount (the host has none of its own).
|
|
22
|
-
*
|
|
23
|
-
* The rule this encodes (2026-09-11): where the host implements attachment
|
|
24
|
-
* upload, the phone composer shows exactly ONE attach control — the host's —
|
|
25
|
-
* and this plugin's S7 button and chips row stand down; on a host without
|
|
26
|
-
* one (DSH ≤ 0.1.2), both load as they always have.
|
|
27
|
-
*
|
|
28
|
-
* `useLayoutEffect`, not `useEffect`: on the composer's very first mount our
|
|
29
|
-
* slot child renders in the same React pass as the host's tool row, so the
|
|
30
|
-
* state initializer below reads the DOM BEFORE either exists — the layout
|
|
31
|
-
* effect re-checks after the commit and before the browser paints, so the
|
|
32
|
-
* demotion to hidden (or, on an old host, the decision to stay) never
|
|
33
|
-
* reaches the screen as a flash. The MutationObserver covers the lifetime
|
|
34
|
-
* after that: the picker's existence only really changes with the page, but
|
|
35
|
-
* a dynamic host plugin or a reload must not strand a stale verdict.
|
|
36
|
-
*/
|
|
37
|
-
export declare function useOwnAttachUi(): boolean;
|
|
38
|
-
//# sourceMappingURL=host-attach.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"host-attach.d.ts","sourceRoot":"","sources":["../../../src/client/host-attach.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,iBAAiB,gHAC2E,CAAA;AAEzG,8EAA8E;AAC9E,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAUxC"}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { useLayoutEffect, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The host composer's own hidden file picker, when the running DSH ships one.
|
|
5
|
-
*
|
|
6
|
-
* DSH 0.1.5 grew a native attachment flow (dsh-attachment / dsh-client-file-
|
|
7
|
-
* upload): the official tool row carries a second `_add` paperclip whose click
|
|
8
|
-
* drives an `<input type="file" hidden>` that is a DIRECT sibling of the
|
|
9
|
-
* buttons inside `_tools`. That input is the one structural, locale-free
|
|
10
|
-
* proof that the host implements composer attachment upload — the button
|
|
11
|
-
* itself can only be told apart from the "+" command menu by its localized
|
|
12
|
-
* aria-label, and the capability prop (`addFiles`) lives on the composer
|
|
13
|
-
* bar's own inject face, which never reaches `conversation.input.left`
|
|
14
|
-
* registrants. Scoped to `_tools` children so a third-party plugin parking
|
|
15
|
-
* its own file input somewhere else in the composer cannot be misread as
|
|
16
|
-
* the official feature; `:not([data-mobile-nav])` keeps OUR picker
|
|
17
|
-
* (`attach-picker`) out of the match.
|
|
18
|
-
*/
|
|
19
|
-
export const HOST_ATTACH_INPUT =
|
|
20
|
-
'[data-slot="conversation.composer.bar"] [class$="_tools"] > input[type="file"]:not([data-mobile-nav])'
|
|
21
|
-
|
|
22
|
-
/** Synchronous read: does this host's composer ship its own attach picker? */
|
|
23
|
-
export function hostAttachPresent(): boolean {
|
|
24
|
-
return document.querySelector(HOST_ATTACH_INPUT) !== null
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* True while OUR attachment UI should mount (the host has none of its own).
|
|
29
|
-
*
|
|
30
|
-
* The rule this encodes (2026-09-11): where the host implements attachment
|
|
31
|
-
* upload, the phone composer shows exactly ONE attach control — the host's —
|
|
32
|
-
* and this plugin's S7 button and chips row stand down; on a host without
|
|
33
|
-
* one (DSH ≤ 0.1.2), both load as they always have.
|
|
34
|
-
*
|
|
35
|
-
* `useLayoutEffect`, not `useEffect`: on the composer's very first mount our
|
|
36
|
-
* slot child renders in the same React pass as the host's tool row, so the
|
|
37
|
-
* state initializer below reads the DOM BEFORE either exists — the layout
|
|
38
|
-
* effect re-checks after the commit and before the browser paints, so the
|
|
39
|
-
* demotion to hidden (or, on an old host, the decision to stay) never
|
|
40
|
-
* reaches the screen as a flash. The MutationObserver covers the lifetime
|
|
41
|
-
* after that: the picker's existence only really changes with the page, but
|
|
42
|
-
* a dynamic host plugin or a reload must not strand a stale verdict.
|
|
43
|
-
*/
|
|
44
|
-
export function useOwnAttachUi(): boolean {
|
|
45
|
-
const [own, setOwn] = useState(() => !hostAttachPresent())
|
|
46
|
-
useLayoutEffect(() => {
|
|
47
|
-
const check = (): void => setOwn(!hostAttachPresent())
|
|
48
|
-
const observer = new MutationObserver(check)
|
|
49
|
-
observer.observe(document.body, { childList: true, subtree: true })
|
|
50
|
-
check()
|
|
51
|
-
return () => observer.disconnect()
|
|
52
|
-
}, [])
|
|
53
|
-
return own
|
|
54
|
-
}
|