dsh-zen-remote 2.3.5 → 2.3.7
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/AGENTS.md +15 -5
- package/CHANGELOG.md +71 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/docs/interface.md +12 -6
- package/docs/plugin-bridge.md +2 -2
- package/docs/remote-access.en.md +9 -3
- package/docs/remote-access.md +9 -3
- package/lan-gate.mjs +7 -0
- package/lib/adapters/dsh-better-sidebar/index.js +31 -2
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
- package/lib/client-pairing.js +10 -0
- package/lib/client-pairing.js.map +1 -1
- package/lib/client-routes.js +183 -94
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +589 -97
- package/lib/debug-redact.js +253 -0
- package/lib/debug-redact.js.map +1 -0
- package/lib/debug-report.js +60 -0
- package/lib/debug-report.js.map +1 -0
- package/lib/fetch-route-intercept.js +23 -0
- package/lib/fetch-route-intercept.js.map +1 -1
- package/lib/index.js +306 -45
- package/lib/index.js.map +1 -1
- package/lib/intercept.js +43 -1
- package/lib/intercept.js.map +1 -1
- package/lib/log-ring.js +76 -0
- package/lib/log-ring.js.map +1 -0
- package/lib/plugin-adapter-core.js +30 -1
- package/lib/plugin-adapter-core.js.map +1 -1
- package/lib/plugin-route-intercept.js +506 -11
- package/lib/plugin-route-intercept.js.map +1 -1
- package/lib/plugin-tunnel.js +210 -18
- package/lib/plugin-tunnel.js.map +1 -1
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +5 -2
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
- package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
- package/lib/types/client/effects/remote-drop.d.ts +20 -8
- package/lib/types/client/effects/remote-drop.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +10 -1
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
- package/lib/types/client/styles/content.css.d.ts +1 -1
- package/lib/types/client/styles/content.css.d.ts.map +1 -1
- package/lib/types/client/styles/layout.css.d.ts +1 -1
- package/lib/types/client/styles/layout.css.d.ts.map +1 -1
- package/lib/types/client/styles/misc.css.d.ts +1 -1
- package/lib/types/client/styles/misc.css.d.ts.map +1 -1
- package/lib/types/client-data/remote-drop.d.ts +64 -0
- package/lib/types/client-data/remote-drop.d.ts.map +1 -1
- package/lib/types/client-data/settings-form.d.ts +3 -0
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/lib/types/client-pairing.d.ts +8 -0
- package/lib/types/client-pairing.d.ts.map +1 -1
- package/lib/types/client-routes.d.ts +23 -0
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/debug-redact.d.ts +68 -0
- package/lib/types/debug-redact.d.ts.map +1 -0
- package/lib/types/debug-report.d.ts +58 -0
- package/lib/types/debug-report.d.ts.map +1 -0
- package/lib/types/fetch-route-intercept.d.ts +26 -0
- package/lib/types/fetch-route-intercept.d.ts.map +1 -1
- package/lib/types/index.d.ts +7 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/intercept.d.ts +6 -0
- package/lib/types/intercept.d.ts.map +1 -1
- package/lib/types/log-ring.d.ts +57 -0
- package/lib/types/log-ring.d.ts.map +1 -0
- package/lib/types/plugin-adapter-core.d.ts.map +1 -1
- package/lib/types/plugin-route-intercept.d.ts +8 -0
- package/lib/types/plugin-route-intercept.d.ts.map +1 -1
- package/lib/types/plugin-tunnel.d.ts +37 -2
- package/lib/types/plugin-tunnel.d.ts.map +1 -1
- package/package.json +3 -2
- package/pwa/app.css +13 -2
- package/scripts/check-debug-report.mjs +277 -0
- package/scripts/check-info-sheet-stacking.mjs +85 -0
- package/scripts/check-remote-drop.mjs +130 -0
- package/scripts/check-tablet-safe-area.mjs +49 -0
- package/src/adapters/dsh-better-sidebar/index.ts +31 -2
- package/src/client/MobileSessionInfo.tsx +29 -2
- package/src/client/effects/remote-drop.ts +271 -24
- package/src/client/locales.ts +24 -4
- package/src/client/settings/SettingsSection.tsx +120 -3
- package/src/client/styles/content.css.ts +44 -14
- package/src/client/styles/layout.css.ts +9 -3
- package/src/client/styles/misc.css.ts +38 -0
- package/src/client-data/remote-drop.ts +103 -0
- package/src/client-data/settings-form.ts +3 -0
- package/src/client-pairing.ts +11 -0
- package/src/client-routes.ts +196 -90
- package/src/debug-redact.ts +290 -0
- package/src/debug-report.ts +93 -0
- package/src/fetch-route-intercept.ts +29 -0
- package/src/index.ts +295 -43
- package/src/intercept.ts +46 -0
- package/src/log-ring.ts +101 -0
- package/src/plugin-adapter-core.ts +29 -2
- package/src/plugin-route-intercept.ts +516 -12
- package/src/plugin-tunnel.ts +230 -20
package/src/intercept.ts
CHANGED
|
@@ -671,6 +671,12 @@ export interface InterceptHandle {
|
|
|
671
671
|
* re-judges NOW — the mergers dedupe no-change calls — instead of
|
|
672
672
|
* waiting for the next frame or the 1s fallback poll. */
|
|
673
673
|
currentSessionChanged(): void
|
|
674
|
+
/** PB-26a: trigger a summary-sync check for virtual session ids discovered
|
|
675
|
+
* OUTSIDE workspace/follow (a sidechat childId the plugin bridge just
|
|
676
|
+
* minted in a response rewrite, for instance). Empty and non-virtual
|
|
677
|
+
* entries are dropped; a batch with nothing left is a no-op. Reaches every
|
|
678
|
+
* live merged `$events` stream, and the pending slot when none is open. */
|
|
679
|
+
nudgeSummarySync(ids: readonly string[]): void
|
|
674
680
|
}
|
|
675
681
|
|
|
676
682
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
@@ -2013,6 +2019,15 @@ async function* mergedEventsStream(deps: MergedEventsStreamDeps): AsyncGenerator
|
|
|
2013
2019
|
/** The ids the in-flight round asked (its triggering batches) — marked at
|
|
2014
2020
|
* completion, joined by nothing (mid-flight batches queue instead). */
|
|
2015
2021
|
let askingNow: string[] = []
|
|
2022
|
+
/** PB-26a: the newest REAL-TIME running flag this stream delivered per
|
|
2023
|
+
* VIRTUAL session id (the leg's forwarded `api-session/status` frames
|
|
2024
|
+
* only — never the synthesized status-sync frames). Read when a row is
|
|
2025
|
+
* first announced: a status frame that reached the UI BEFORE its row
|
|
2026
|
+
* existed was dropped by the host (handleSessionStatus only updates
|
|
2027
|
+
* sessions already in its summaries), and a flip during the list pull is
|
|
2028
|
+
* newer than the snapshot's `running` — either way the announce must be
|
|
2029
|
+
* followed by the live value. Cleared with a fresh serving period. */
|
|
2030
|
+
const latestLiveStatus = new Map<string, boolean>()
|
|
2016
2031
|
const scheduleRetry = (): void => {
|
|
2017
2032
|
if (retryTimer !== undefined || retryAt.size === 0) return
|
|
2018
2033
|
const earliest = Math.min(...retryAt.values())
|
|
@@ -2072,6 +2087,15 @@ async function* mergedEventsStream(deps: MergedEventsStreamDeps): AsyncGenerator
|
|
|
2072
2087
|
if (announced.has(rowId)) continue
|
|
2073
2088
|
announced.add(rowId)
|
|
2074
2089
|
channel.push(frame)
|
|
2090
|
+
// PB-26a: the row is born in the UI carrying the snapshot's
|
|
2091
|
+
// `running`; when this stream delivered a live status for it the
|
|
2092
|
+
// host could not apply (no row yet) or that postdates the
|
|
2093
|
+
// snapshot, realign right behind the added frame — only when the
|
|
2094
|
+
// two disagree, a matching flag needs no second frame.
|
|
2095
|
+
const liveRunning = latestLiveStatus.get(rowId)
|
|
2096
|
+
if (liveRunning !== undefined && liveRunning !== (isPlainObject(row) && row.running === true)) {
|
|
2097
|
+
channel.push({ type: 'emit', event: 'api-session/status', args: [rowId, liveRunning] })
|
|
2098
|
+
}
|
|
2075
2099
|
}
|
|
2076
2100
|
// T65-fix ask bookkeeping: a returned id is announced and done; an
|
|
2077
2101
|
// id the server did NOT return either exhausts its attempts or
|
|
@@ -2206,6 +2230,10 @@ async function* mergedEventsStream(deps: MergedEventsStreamDeps): AsyncGenerator
|
|
|
2206
2230
|
askedForGood.clear()
|
|
2207
2231
|
askAttempts.clear()
|
|
2208
2232
|
retryAt.clear()
|
|
2233
|
+
// PB-26a: the live flags belong to the serving period that delivered
|
|
2234
|
+
// them — the status re-sync below re-reads every row from a fresh
|
|
2235
|
+
// snapshot.
|
|
2236
|
+
latestLiveStatus.clear()
|
|
2209
2237
|
if (retryTimer !== undefined) {
|
|
2210
2238
|
clock.clearTimeout(retryTimer)
|
|
2211
2239
|
retryTimer = undefined
|
|
@@ -2370,6 +2398,20 @@ async function* mergedEventsStream(deps: MergedEventsStreamDeps): AsyncGenerator
|
|
|
2370
2398
|
if (isPlainObject(frame) && frame.type === 'emit' && frame.event === 'api-session/status') {
|
|
2371
2399
|
const rowId = Array.isArray(frame.args) ? frame.args[0] : undefined
|
|
2372
2400
|
if (typeof rowId === 'string' && rowId !== '') liveStatusSeq.set(rowId, ++statusSeq)
|
|
2401
|
+
// PB-26a: the rewritten frame is the proved shape (`args` =
|
|
2402
|
+
// [virtualId, running]). Remember the live flag under the
|
|
2403
|
+
// VIRTUAL id, and — when this stream has not announced the row
|
|
2404
|
+
// — pull the list now: a sidechat / subagent child rides no
|
|
2405
|
+
// workspace/follow frame, so this status is the client's first
|
|
2406
|
+
// (and, until the next turn, only) notice that the row exists.
|
|
2407
|
+
// The frame itself still crosses at once, below.
|
|
2408
|
+
const args = isPlainObject(rewritten) && Array.isArray(rewritten.args) ? rewritten.args : undefined
|
|
2409
|
+
const virtualId = args?.[0]
|
|
2410
|
+
const running = args?.[1]
|
|
2411
|
+
if (typeof virtualId === 'string' && typeof running === 'boolean') {
|
|
2412
|
+
latestLiveStatus.set(virtualId, running)
|
|
2413
|
+
if (!announced.has(virtualId)) handleForwardedBatch([virtualId])
|
|
2414
|
+
}
|
|
2373
2415
|
}
|
|
2374
2416
|
// The orphan-close bookkeeping rides the two id-carrying frame
|
|
2375
2417
|
// types: waterfalls enter the record (once per leg — a repeat
|
|
@@ -3498,6 +3540,10 @@ export function installIntercept(options: InstallInterceptOptions): InterceptHan
|
|
|
3498
3540
|
currentSessionChanged() {
|
|
3499
3541
|
for (const listener of [...currentSessionListeners]) listener()
|
|
3500
3542
|
},
|
|
3543
|
+
nudgeSummarySync(ids: readonly string[]) {
|
|
3544
|
+
const virtualIds = ids.filter((id) => typeof id === 'string' && id !== '' && isVirtual(id))
|
|
3545
|
+
if (virtualIds.length > 0) summarySync.noteForwardedSessionIds(virtualIds)
|
|
3546
|
+
},
|
|
3501
3547
|
}
|
|
3502
3548
|
}
|
|
3503
3549
|
|
package/src/log-ring.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CU-1: the plugin's in-process diagnostic log ring (SPEC §方案 C). A bounded
|
|
3
|
+
* FIFO of the plugin row's own log lines, kept in memory only — the desktop
|
|
4
|
+
* app writes no log file, so this is the one artifact the debug report (CU-2)
|
|
5
|
+
* can read on either role. Deliberately NOT a file, and deliberately only
|
|
6
|
+
* this plugin's lines: DSH's own log stream stays untouched.
|
|
7
|
+
*
|
|
8
|
+
* Pure module: no DSH imports, no I/O, no clocks beyond the timestamp it
|
|
9
|
+
* stamps entries with. The wiring lives in src/index.ts, which funnels its
|
|
10
|
+
* `logWarn` / `logInfo` through {@link createPluginLog} so existing call
|
|
11
|
+
* sites enter the ring unchanged (double write: logger first, ring second),
|
|
12
|
+
* and pushes the handful of lifecycle events (row apply/reload, gateway
|
|
13
|
+
* spawn/exit, relay link flips, fetch-route installs) by hand.
|
|
14
|
+
*
|
|
15
|
+
* One discipline the debug report leans on: ring lines carry event codes,
|
|
16
|
+
* id shapes and counters — never payloads. The redactor (debug-redact.ts)
|
|
17
|
+
* is the second layer, not the license to log secrets here.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { format } from 'node:util'
|
|
21
|
+
|
|
22
|
+
/** Severity of one ring entry. The ring only distinguishes what the report
|
|
23
|
+
* renders: info lines and warnings. */
|
|
24
|
+
export type LogLevel = 'info' | 'warn'
|
|
25
|
+
|
|
26
|
+
/** One ring entry: ISO timestamp, level, fully interpolated message. */
|
|
27
|
+
export interface LogRingEntry {
|
|
28
|
+
t: string
|
|
29
|
+
level: LogLevel
|
|
30
|
+
msg: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Longest message kept, in characters (UTF-16 code units). Debug lines are
|
|
34
|
+
* codes and shapes, not dumps — anything longer was the wrong thing to log. */
|
|
35
|
+
const MAX_MSG_CHARS = 512
|
|
36
|
+
|
|
37
|
+
/** The ring's face. `push` never throws and never returns anything; callers
|
|
38
|
+
* log through it exactly like they would through the context logger. */
|
|
39
|
+
export interface LogRing {
|
|
40
|
+
push(level: LogLevel, formatText: string, ...args: unknown[]): void
|
|
41
|
+
/** Oldest-last copy of the ring. Mutating the returned array or its
|
|
42
|
+
* entries never reaches the ring's own state. */
|
|
43
|
+
snapshot(): LogRingEntry[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create one bounded log ring. `cap` defaults to 300 entries — a full debug
|
|
48
|
+
* session of lifecycle lines plus a healthy share of warnings, while keeping
|
|
49
|
+
* the report a fixed small size. A cap below 1 is clamped to 1.
|
|
50
|
+
*/
|
|
51
|
+
export function createLogRing(cap = 300): LogRing {
|
|
52
|
+
const capacity = Number.isFinite(cap) && cap >= 1 ? Math.floor(cap) : 1
|
|
53
|
+
const entries: LogRingEntry[] = []
|
|
54
|
+
return {
|
|
55
|
+
push(level, formatText, ...args) {
|
|
56
|
+
let msg: string
|
|
57
|
+
try {
|
|
58
|
+
msg = args.length === 0 ? String(formatText) : format(formatText, ...args)
|
|
59
|
+
} catch {
|
|
60
|
+
// A malformed format string must never break the caller's flow —
|
|
61
|
+
// keep the raw template as the message.
|
|
62
|
+
msg = String(formatText)
|
|
63
|
+
}
|
|
64
|
+
if (msg.length > MAX_MSG_CHARS) msg = msg.slice(0, MAX_MSG_CHARS)
|
|
65
|
+
entries.push({ t: new Date().toISOString(), level, msg })
|
|
66
|
+
if (entries.length > capacity) entries.splice(0, entries.length - capacity)
|
|
67
|
+
},
|
|
68
|
+
snapshot() {
|
|
69
|
+
return entries.map((entry) => ({ ...entry }))
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The logger face this module needs: the cordis context logger's two
|
|
75
|
+
* methods, each optional (test fakes carry none). */
|
|
76
|
+
export interface PluginLogLike {
|
|
77
|
+
info?(format: string, ...args: unknown[]): void
|
|
78
|
+
warn?(format: string, ...args: unknown[]): void
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The double-writer the apply() wiring funnels its logging through: every
|
|
82
|
+
* line reaches the context logger (unchanged behavior, unchanged call
|
|
83
|
+
* sites) AND the ring (the debug report's source). A ring failure is
|
|
84
|
+
* swallowed; a logger failure propagates exactly as it did before this
|
|
85
|
+
* module existed. */
|
|
86
|
+
export function createPluginLog(ring: LogRing, logger: PluginLogLike | undefined): { info(format: string, ...args: unknown[]): void, warn(format: string, ...args: unknown[]): void } {
|
|
87
|
+
return {
|
|
88
|
+
info(formatText, ...args) {
|
|
89
|
+
logger?.info?.(formatText, ...args)
|
|
90
|
+
try {
|
|
91
|
+
ring.push('info', formatText, ...args)
|
|
92
|
+
} catch { /* the ring must never break logging */ }
|
|
93
|
+
},
|
|
94
|
+
warn(formatText, ...args) {
|
|
95
|
+
logger?.warn?.(formatText, ...args)
|
|
96
|
+
try {
|
|
97
|
+
ring.push('warn', formatText, ...args)
|
|
98
|
+
} catch { /* the ring must never break logging */ }
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { createHash } from 'node:crypto'
|
|
26
26
|
import { isVirtual, fromVirtual } from './virtual-id.js'
|
|
27
27
|
import { MAX_UPLOAD_BYTES } from './upload-limits.js'
|
|
28
|
-
import type { NormalizedBridgeRoute, NormalizedDeclaration } from './plugin-bridge.js'
|
|
28
|
+
import type { BridgeLocator, NormalizedBridgeRoute, NormalizedDeclaration } from './plugin-bridge.js'
|
|
29
29
|
import { BRIDGE_METHODS, DEFAULT_ROUTE_BODY_BYTES } from './plugin-bridge.js'
|
|
30
30
|
|
|
31
31
|
export { fromVirtual }
|
|
@@ -327,6 +327,15 @@ export function normalizeAdapter(input: unknown, installedVersion: string, optio
|
|
|
327
327
|
methods: route.methods.length === 1 && route.methods[0] === 'GET' ? undefined : [...route.methods],
|
|
328
328
|
maxBody: route.maxBody === DEFAULT_ROUTE_BODY_BYTES ? undefined : route.maxBody,
|
|
329
329
|
upgrade: route.upgrade ? true : undefined,
|
|
330
|
+
// PB-26b: an upgrade route whose ids do NOT ride the query keeps its
|
|
331
|
+
// locator on the registry entry — the tunnel's validation and the
|
|
332
|
+
// client's upgrade surrogate read where the ids actually arrive (the
|
|
333
|
+
// sidechat ws watch carries them in the post-handshake JSON frames).
|
|
334
|
+
// A query route's declaration stays EXACTLY the 2.1 shape — no
|
|
335
|
+
// locator key — so every older consumer and digest is untouched
|
|
336
|
+
// (the adapter digest always covered route.locator and never
|
|
337
|
+
// covered the declaration routes' shape).
|
|
338
|
+
...(route.upgrade && route.locator.in !== 'query' ? { locator: route.locator as BridgeLocator } : {}),
|
|
330
339
|
})),
|
|
331
340
|
digest,
|
|
332
341
|
}
|
|
@@ -483,6 +492,16 @@ export function locateAdapterIds(route: NormalizedAdapterRoute, pathname: string
|
|
|
483
492
|
for (const field of locator.fields) {
|
|
484
493
|
const value = body[field]
|
|
485
494
|
if (typeof value === 'string' && value !== '') hits.push({ kind: 'json-body', key: field, value })
|
|
495
|
+
// PB-26b: a field may carry a LIST of ids — better-sidebar's sidechat
|
|
496
|
+
// ws watch frame sends `{"op":"watch","threadIds":[...]}`. Every
|
|
497
|
+
// non-empty string element is one hit under the same field key, so
|
|
498
|
+
// classify/rewrite treat the batch exactly like repeated query
|
|
499
|
+
// values: every id virtual → remote, one mixed in → refuse.
|
|
500
|
+
else if (Array.isArray(value)) {
|
|
501
|
+
for (const item of value) {
|
|
502
|
+
if (typeof item === 'string' && item !== '') hits.push({ kind: 'json-body', key: field, value: item })
|
|
503
|
+
}
|
|
504
|
+
}
|
|
486
505
|
}
|
|
487
506
|
}
|
|
488
507
|
}
|
|
@@ -549,7 +568,15 @@ export function rewriteAdapterRequest(
|
|
|
549
568
|
} else {
|
|
550
569
|
if (bodyCopy === undefined && isRecord(body)) bodyCopy = { ...body }
|
|
551
570
|
if (bodyCopy !== undefined) {
|
|
552
|
-
bodyCopy[hit.key!]
|
|
571
|
+
const current = bodyCopy[hit.key!]
|
|
572
|
+
// PB-26b: an array field rewrites IN PLACE, element for element —
|
|
573
|
+
// only the entries equal to this hit's located value swap for their
|
|
574
|
+
// real halves, so a mixed batch of ids keeps its shape and order.
|
|
575
|
+
if (Array.isArray(current)) {
|
|
576
|
+
bodyCopy[hit.key!] = current.map((item) => (item === hit.value ? real : item))
|
|
577
|
+
} else {
|
|
578
|
+
bodyCopy[hit.key!] = real
|
|
579
|
+
}
|
|
553
580
|
bodyText = JSON.stringify(bodyCopy)
|
|
554
581
|
}
|
|
555
582
|
}
|