@linxin666/dsh-remote-web-ui 0.4.3 → 0.4.5
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.i18n.yaml +2 -2
- package/README.md +23 -8
- package/README.zh.md +22 -7
- package/lib/client.js +194 -140
- package/lib/client.js.map +1 -1
- package/lib/index.js +231 -23
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +2 -1
- package/lib/types/client/index.js +3 -3
- package/lib/types/client/locales.d.ts +0 -4
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +5 -9
- package/lib/types/client/mobile-adapt.d.ts +2 -2
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +163 -136
- package/lib/types/client/settings-form.d.ts +16 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +27 -1
- package/lib/types/detached-work.d.ts +45 -0
- package/lib/types/detached-work.d.ts.map +1 -0
- package/lib/types/detached-work.js +54 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +67 -12
- package/lib/types/lan-bind.d.ts +21 -4
- package/lib/types/lan-bind.d.ts.map +1 -1
- package/lib/types/lan-bind.js +103 -17
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +43 -0
- package/lib/types/remote-api.d.ts +13 -1
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +14 -1
- package/package.json +19 -19
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/RemoteSettingsCard.tsx +2 -0
- package/src/client/index.ts +3 -3
- package/src/client/locales.ts +5 -9
- package/src/client/mobile-adapt.ts +167 -138
- package/src/client/settings-card.module.css +31 -5
- package/src/client/settings-form.ts +27 -1
- package/src/detached-work.ts +57 -0
- package/src/index.ts +67 -12
- package/src/lan-bind.ts +111 -17
- package/src/pairing.ts +44 -1
- package/src/remote-api.ts +14 -1
package/lib/types/index.js
CHANGED
|
@@ -29,6 +29,7 @@ import { withIdentityEncoding } from "./http.js";
|
|
|
29
29
|
import { TunnelManager } from "./tunnel.js";
|
|
30
30
|
import { PublicBaseKeeper } from "./public-base.js";
|
|
31
31
|
import { mountOnce } from "./mount-once.js";
|
|
32
|
+
import { runDetached } from "./detached-work.js";
|
|
32
33
|
import { REMOTE_CHANNEL_BOOT_SCRIPT } from "./remote-channel-boot.js";
|
|
33
34
|
import { UUID_POLYFILL_SCRIPT } from "./uuid-polyfill.js";
|
|
34
35
|
/** Stable cordis plugin name. */
|
|
@@ -536,18 +537,34 @@ function applyImpl(ctx, config) {
|
|
|
536
537
|
if (ctx.webServer.host === '0.0.0.0') {
|
|
537
538
|
console.warn('remote-web-ui: LAN-exposed bind — pairing gates the /remote channel; direct /api stays under the harness fence + browser auth (stop() does not revoke an already-redeemed browser credential)');
|
|
538
539
|
}
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
540
|
+
// LAN bind toggle: only write the managed patch block once the user has
|
|
541
|
+
// flipped it (undefined = untouched, never write). Desired host/port come
|
|
542
|
+
// from the CLI flags when given (flags win), else from the toggle and
|
|
543
|
+
// the currently bound port. The block takes effect on the next start, so
|
|
544
|
+
// the re-assert at every boot keeps it in sync with both the toggle and
|
|
545
|
+
// the flags.
|
|
546
|
+
//
|
|
547
|
+
// cordis.patch.yml is exactly the file the Host's HMR config watcher
|
|
548
|
+
// refreshes from, and a settings save runs inside the Host's exclusive HMR
|
|
549
|
+
// transaction. Writing that file while the caller's transaction is open makes
|
|
550
|
+
// the watcher's refresh re-enter it, which the Host refuses with "HMR
|
|
551
|
+
// transactions cannot be nested" and which surfaces on the user's save
|
|
552
|
+
// (#1751, #1754).
|
|
553
|
+
//
|
|
554
|
+
// Deferring alone does NOT escape that context, and the earlier belt rested
|
|
555
|
+
// on a premise that measurement disproves: AsyncLocalStorage is propagated
|
|
556
|
+
// into setImmediate, into node:timers, and into AsyncResource scopes (a
|
|
557
|
+
// plain resource built while the transaction runs inherits it), so
|
|
558
|
+
// "setImmediate starts a fresh store" is false. What does start clean is an
|
|
559
|
+
// AsyncResource created at module scope, before any transaction existed -
|
|
560
|
+
// see detached-work.ts. scheduleLanBindWork runs the scheduling through it,
|
|
561
|
+
// so the write lands outside the caller's transaction.
|
|
562
|
+
//
|
|
563
|
+
// The assertion is also idempotent on its own: the write is guarded by the
|
|
564
|
+
// current-vs-desired comparison below, so once the profile carries the block
|
|
565
|
+
// a coalesced follow-up sync() finds nothing to do. That guard is a second
|
|
566
|
+
// belt, not the fix.
|
|
567
|
+
const applyLanBindWork = (value) => {
|
|
551
568
|
if (value.lanBind !== undefined) {
|
|
552
569
|
const startup = ctx.get('webStartup');
|
|
553
570
|
const desiredHost = desiredBindHost(value.lanBind === true, startup?.host);
|
|
@@ -589,6 +606,44 @@ function applyImpl(ctx, config) {
|
|
|
589
606
|
}
|
|
590
607
|
}
|
|
591
608
|
}
|
|
609
|
+
};
|
|
610
|
+
// The LAN bind assertion never blocks the caller: one pending work item is
|
|
611
|
+
// enough, and it always re-reads the config through `resolve()` at run time
|
|
612
|
+
// so a toggle flipped twice before the tick lands on the last value.
|
|
613
|
+
//
|
|
614
|
+
// The scheduling itself runs through runDetached: the settings save that
|
|
615
|
+
// drives sync() holds the Host's exclusive HMR transaction in its async
|
|
616
|
+
// context, and a `setImmediate` scheduled directly from here would inherit
|
|
617
|
+
// that mark (measured on Node 24), so the write would still land inside the
|
|
618
|
+
// transaction the watcher then tries to re-enter. Scheduling from the
|
|
619
|
+
// module-scope resource starts the timer - and therefore the write - with no
|
|
620
|
+
// store at all.
|
|
621
|
+
let lanBindWorkPending = false;
|
|
622
|
+
const scheduleLanBindWork = () => {
|
|
623
|
+
if (lanBindWorkPending)
|
|
624
|
+
return;
|
|
625
|
+
lanBindWorkPending = true;
|
|
626
|
+
runDetached(() => setImmediate(() => {
|
|
627
|
+
lanBindWorkPending = false;
|
|
628
|
+
try {
|
|
629
|
+
applyLanBindWork(resolve());
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
// A deferred assertion must never surface as an unhandled rejection:
|
|
633
|
+
// the settings save that triggered it already answered, and the card
|
|
634
|
+
// reads the live block state back on its own poll.
|
|
635
|
+
console.error(`remote-web-ui: the deferred lan-bind assertion failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
636
|
+
}
|
|
637
|
+
}));
|
|
638
|
+
};
|
|
639
|
+
ctx.effect(() => () => { lanBindWorkPending = false; }, 'remote-web-ui: lan-bind work');
|
|
640
|
+
// Apply the effective configuration to the running surfaces once per
|
|
641
|
+
// activation: the service tunables, the LAN-bind block/firewall, the tunnel
|
|
642
|
+
// plan, the pairing routes, the presence sweep, and the posture probe.
|
|
643
|
+
const sync = () => {
|
|
644
|
+
const value = resolve();
|
|
645
|
+
service.config = pairingConfigOf(value);
|
|
646
|
+
scheduleLanBindWork();
|
|
592
647
|
// The plugin-managed tunnels own the public base while one runs: the URL
|
|
593
648
|
// lands in the service through the tunnel's phase listener (the minted
|
|
594
649
|
// quick URL, or the named tunnel's fixed public hostname). The manual
|
package/lib/types/lan-bind.d.ts
CHANGED
|
@@ -30,7 +30,23 @@ export type LanBindHost = '0.0.0.0' | '127.0.0.1';
|
|
|
30
30
|
* profiles directory.
|
|
31
31
|
*/
|
|
32
32
|
export declare function profilePatchFile(profile: string, home?: string): string;
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Remove the managed block (its markers and the webserver row this plugin
|
|
35
|
+
* wrote) from patch content, keeping every other row.
|
|
36
|
+
*
|
|
37
|
+
* The markers travel with the file through writers that append rows through
|
|
38
|
+
* the YAML document API: the appended rows land at the end of the sequence,
|
|
39
|
+
* which is BEFORE a trailing END comment, so the region between the markers
|
|
40
|
+
* can hold rows this plugin never wrote. Deleting the whole region silently
|
|
41
|
+
* removed the user's model, plugin and remote-control rows on the next rewrite
|
|
42
|
+
* - the remote-control row carries the LAN toggle itself, so the feature reset
|
|
43
|
+
* to `undefined` and the phone was locked out again after a restart (DSH
|
|
44
|
+
* Desktop report, 2026-09-30). Only the managed item is removed here; every
|
|
45
|
+
* foreign row is kept in place and the fresh block is appended after them (the
|
|
46
|
+
* same-id row last wins).
|
|
47
|
+
* @param content - the patch file text.
|
|
48
|
+
* @returns the text with the markers and the managed row removed.
|
|
49
|
+
*/
|
|
34
50
|
export declare function stripManagedBlock(content: string): string;
|
|
35
51
|
/**
|
|
36
52
|
* Render the managed block for one bind state. The values are static: the
|
|
@@ -61,9 +77,10 @@ export declare function lanBindState(profile: string, home?: string): {
|
|
|
61
77
|
* patch file is preserved; the block is rewritten atomically (unique temp
|
|
62
78
|
* file + rename, so concurrent writers can never rename a half-written
|
|
63
79
|
* file) with the original file's permissions preserved. A hand-truncated
|
|
64
|
-
* unterminated block (BEGIN marker without END
|
|
65
|
-
*
|
|
66
|
-
* never stack a second webserver row onto the
|
|
80
|
+
* unterminated block (BEGIN marker without END) is consumed by
|
|
81
|
+
* stripManagedBlock as well - markers and managed row removed, foreign rows
|
|
82
|
+
* kept - so the rewrite can never stack a second webserver row onto the
|
|
83
|
+
* orphan.
|
|
67
84
|
*
|
|
68
85
|
* The written file is always exactly one valid YAML document: a base the
|
|
69
86
|
* plugin manager left in flow style is re-emitted as a block sequence before
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lan-bind.d.ts","sourceRoot":"","sources":["../../src/lan-bind.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,eAAO,MAAM,oBAAoB,mEAAmE,CAAA;AACpG,eAAO,MAAM,kBAAkB,+CAA+C,CAAA;AAE9E,iDAAiD;AACjD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,WAAW,CAAA;AAEjD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAUlF;
|
|
1
|
+
{"version":3,"file":"lan-bind.d.ts","sourceRoot":"","sources":["../../src/lan-bind.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,eAAO,MAAM,oBAAoB,mEAAmE,CAAA;AACpG,eAAO,MAAM,kBAAkB,+CAA+C,CAAA;AAE9E,iDAAiD;AACjD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,WAAW,CAAA;AAEjD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAUlF;AAqED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAoBzD;AA0DD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAcpE;AAED,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;IACjC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;CACzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAkB3E;AAED,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG;IAAE,YAAY,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAI/H;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,MAAkB,GAAG,IAAI,CAW7G"}
|
package/lib/types/lan-bind.js
CHANGED
|
@@ -49,16 +49,96 @@ function readPatchContent(file) {
|
|
|
49
49
|
return '';
|
|
50
50
|
return readFileSync(file, 'utf8');
|
|
51
51
|
}
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Split a block-sequence region into its top-level items. Only a column-0 `-`
|
|
54
|
+
* starts an item: that is the shape every writer of this file produces - the
|
|
55
|
+
* managed block itself and the YAML document API's block-style re-emission.
|
|
56
|
+
* @param region - text between two line boundaries.
|
|
57
|
+
* @returns the items in source order; text outside them is not returned.
|
|
58
|
+
*/
|
|
59
|
+
function blockItems(region) {
|
|
60
|
+
const items = [];
|
|
61
|
+
let start = -1;
|
|
62
|
+
let offset = 0;
|
|
63
|
+
for (const line of region.split('\n')) {
|
|
64
|
+
if (line.startsWith('- ') || line === '-') {
|
|
65
|
+
if (start !== -1)
|
|
66
|
+
items.push({ start, end: offset, text: region.slice(start, offset) });
|
|
67
|
+
start = offset;
|
|
68
|
+
}
|
|
69
|
+
offset += line.length + 1;
|
|
70
|
+
}
|
|
71
|
+
if (start !== -1)
|
|
72
|
+
items.push({ start, end: region.length, text: region.slice(start) });
|
|
73
|
+
return items;
|
|
55
74
|
}
|
|
56
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Whether one top-level item is the webserver row this plugin's block owns.
|
|
77
|
+
* The row id is the whole signal: the markers declare everything inside them
|
|
78
|
+
* managed, our block always writes `- id: webserver`, and a same-id row later
|
|
79
|
+
* in the patch replaces this one wholesale anyway - so an id-webserver row
|
|
80
|
+
* found inside our markers is this plugin's row (a hand-truncated block may
|
|
81
|
+
* have lost its `name` line).
|
|
82
|
+
* @param item - one top-level item's text.
|
|
83
|
+
* @returns true when the item is the managed webserver row.
|
|
84
|
+
*/
|
|
85
|
+
function isManagedWebserverItem(item) {
|
|
86
|
+
return /^- id:\s*['"]?webserver['"]?[ \t]*(?:\r?\n|$)/.test(item);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The text between a block's BEGIN and END markers: from the character after
|
|
90
|
+
* the BEGIN line's terminator to the start of the END marker's line (or to
|
|
91
|
+
* the end of the content without an END marker).
|
|
92
|
+
* @param content - the patch file text.
|
|
93
|
+
* @param begin - index of the BEGIN marker.
|
|
94
|
+
* @param end - index of the END marker, or -1 when the block is unterminated.
|
|
95
|
+
* @returns the region's text.
|
|
96
|
+
*/
|
|
97
|
+
function managedRegion(content, begin, end) {
|
|
98
|
+
const newlineAfterBegin = content.indexOf('\n', begin);
|
|
99
|
+
const innerStart = newlineAfterBegin === -1 ? content.length : newlineAfterBegin + 1;
|
|
100
|
+
const endLineStart = end === -1 ? content.length : content.lastIndexOf('\n', end - 1) + 1;
|
|
101
|
+
return content.slice(innerStart, Math.max(innerStart, endLineStart));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Remove the managed block (its markers and the webserver row this plugin
|
|
105
|
+
* wrote) from patch content, keeping every other row.
|
|
106
|
+
*
|
|
107
|
+
* The markers travel with the file through writers that append rows through
|
|
108
|
+
* the YAML document API: the appended rows land at the end of the sequence,
|
|
109
|
+
* which is BEFORE a trailing END comment, so the region between the markers
|
|
110
|
+
* can hold rows this plugin never wrote. Deleting the whole region silently
|
|
111
|
+
* removed the user's model, plugin and remote-control rows on the next rewrite
|
|
112
|
+
* - the remote-control row carries the LAN toggle itself, so the feature reset
|
|
113
|
+
* to `undefined` and the phone was locked out again after a restart (DSH
|
|
114
|
+
* Desktop report, 2026-09-30). Only the managed item is removed here; every
|
|
115
|
+
* foreign row is kept in place and the fresh block is appended after them (the
|
|
116
|
+
* same-id row last wins).
|
|
117
|
+
* @param content - the patch file text.
|
|
118
|
+
* @returns the text with the markers and the managed row removed.
|
|
119
|
+
*/
|
|
57
120
|
export function stripManagedBlock(content) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
121
|
+
let text = content;
|
|
122
|
+
for (;;) {
|
|
123
|
+
const begin = text.indexOf(LAN_BIND_BLOCK_BEGIN);
|
|
124
|
+
if (begin === -1)
|
|
125
|
+
return text;
|
|
126
|
+
const end = text.indexOf(LAN_BIND_BLOCK_END, begin);
|
|
127
|
+
const blockStart = text.lastIndexOf('\n', begin - 1) + 1;
|
|
128
|
+
const newlineAfterEnd = end === -1 ? -1 : text.indexOf('\n', end);
|
|
129
|
+
const resumeAt = end === -1 ? text.length : (newlineAfterEnd === -1 ? text.length : newlineAfterEnd + 1);
|
|
130
|
+
const region = managedRegion(text, begin, end);
|
|
131
|
+
let kept = '';
|
|
132
|
+
let cursor = 0;
|
|
133
|
+
for (const item of blockItems(region)) {
|
|
134
|
+
if (!isManagedWebserverItem(item.text))
|
|
135
|
+
continue;
|
|
136
|
+
kept += region.slice(cursor, item.start);
|
|
137
|
+
cursor = item.end;
|
|
138
|
+
}
|
|
139
|
+
kept += region.slice(cursor);
|
|
140
|
+
text = text.slice(0, blockStart) + kept + text.slice(resumeAt);
|
|
141
|
+
}
|
|
62
142
|
}
|
|
63
143
|
/**
|
|
64
144
|
* Render the managed block for one bind state. The values are static: the
|
|
@@ -149,9 +229,16 @@ export function managedBindOf(content) {
|
|
|
149
229
|
if (begin === -1)
|
|
150
230
|
return undefined;
|
|
151
231
|
const end = content.indexOf(LAN_BIND_BLOCK_END, begin);
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
232
|
+
const region = managedRegion(content, begin, end);
|
|
233
|
+
// Read the bind out of the managed row itself. A region a YAML writer
|
|
234
|
+
// displaced holds foreign rows too, and their `host`/`port` keys are not
|
|
235
|
+
// this block's bind - reading them made the boot re-assert
|
|
236
|
+
// believe the block was already applied. A region without a recognizable
|
|
237
|
+
// managed row (hand-edited literals) keeps the historical whole-region read.
|
|
238
|
+
const items = blockItems(region).filter(item => isManagedWebserverItem(item.text));
|
|
239
|
+
const scope = items.length > 0 ? items.map(item => item.text).join('') : region;
|
|
240
|
+
const hostMatch = /host:\s*'([^']+)'/.exec(scope);
|
|
241
|
+
const portMatch = /port:\s*(\d+)/.exec(scope);
|
|
155
242
|
return {
|
|
156
243
|
host: hostMatch?.[1] ?? '',
|
|
157
244
|
port: portMatch !== null ? Number(portMatch[1]) : undefined,
|
|
@@ -169,9 +256,10 @@ export function lanBindState(profile, home = dshHome()) {
|
|
|
169
256
|
* patch file is preserved; the block is rewritten atomically (unique temp
|
|
170
257
|
* file + rename, so concurrent writers can never rename a half-written
|
|
171
258
|
* file) with the original file's permissions preserved. A hand-truncated
|
|
172
|
-
* unterminated block (BEGIN marker without END
|
|
173
|
-
*
|
|
174
|
-
* never stack a second webserver row onto the
|
|
259
|
+
* unterminated block (BEGIN marker without END) is consumed by
|
|
260
|
+
* stripManagedBlock as well - markers and managed row removed, foreign rows
|
|
261
|
+
* kept - so the rewrite can never stack a second webserver row onto the
|
|
262
|
+
* orphan.
|
|
175
263
|
*
|
|
176
264
|
* The written file is always exactly one valid YAML document: a base the
|
|
177
265
|
* plugin manager left in flow style is re-emitted as a block sequence before
|
|
@@ -179,9 +267,7 @@ export function lanBindState(profile, home = dshHome()) {
|
|
|
179
267
|
*/
|
|
180
268
|
export function writeLanBind(host, port, profile, home = dshHome()) {
|
|
181
269
|
const file = profilePatchFile(profile, home);
|
|
182
|
-
const
|
|
183
|
-
const orphanBegin = stripped.indexOf(LAN_BIND_BLOCK_BEGIN);
|
|
184
|
-
const rawBase = (orphanBegin === -1 ? stripped : stripped.slice(0, orphanBegin)).trimEnd();
|
|
270
|
+
const rawBase = stripManagedBlock(readPatchContent(file)).trimEnd();
|
|
185
271
|
const base = rawBase.replace(/\[\s*\]\s*$/, '').trimEnd();
|
|
186
272
|
const block = managedBlock(host, port);
|
|
187
273
|
const content = mergePatchContent(base, block, file);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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"}
|
package/lib/types/pairing.js
CHANGED
|
@@ -509,6 +509,7 @@ function snapshotsEqual(a, b) {
|
|
|
509
509
|
&& a.publicUrl === b.publicUrl
|
|
510
510
|
&& tunnelEqual(a.tunnel, b.tunnel)
|
|
511
511
|
&& relayEqual(a.relay, b.relay)
|
|
512
|
+
&& postureEqual(a.posture, b.posture)
|
|
512
513
|
&& a.tokenId === b.tokenId
|
|
513
514
|
&& a.tokenExpiresAt === b.tokenExpiresAt
|
|
514
515
|
&& a.deviceCount === b.deviceCount
|
|
@@ -537,6 +538,48 @@ function relayEqual(a, b) {
|
|
|
537
538
|
return a === b || (a !== undefined && b !== undefined
|
|
538
539
|
&& a.state === b.state && a.url === b.url && a.error === b.error);
|
|
539
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* Posture equality. A probe round can change this frame while every other field
|
|
543
|
+
* stays put — the common case, since the probe re-runs on a settings change and
|
|
544
|
+
* on a tunnel reaching running, and a quick-tunnel restart churns the probed
|
|
545
|
+
* host set. Leaving the frame out of the comparison suppressed those emits, so
|
|
546
|
+
* the loopback SSE stream and the desktop panel could sit on a stale "exposed"
|
|
547
|
+
* (or stale "clean") verdict for as long as the phase and roster stayed put.
|
|
548
|
+
*/
|
|
549
|
+
function postureEqual(a, b) {
|
|
550
|
+
if (a === b)
|
|
551
|
+
return true;
|
|
552
|
+
// A frame that is not an object at all (a hand-built context can pass
|
|
553
|
+
// anything) counts as changed instead of throwing out of the setter.
|
|
554
|
+
if (!isPostureFrame(a) || !isPostureFrame(b))
|
|
555
|
+
return false;
|
|
556
|
+
return a.checkedAt === b.checkedAt && hostsEqual(a.hosts, b.hosts);
|
|
557
|
+
}
|
|
558
|
+
/** Whether a value has the shape a posture frame is compared on. */
|
|
559
|
+
function isPostureFrame(value) {
|
|
560
|
+
return typeof value === 'object' && value !== null;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Per-host posture equality (a round's verdicts, in probe order). Tolerates a
|
|
564
|
+
* frame that is not the expected shape: this runs inside the emit path, outside
|
|
565
|
+
* the listener error containment, so a malformed snapshot handed in by another
|
|
566
|
+
* host-fiber caller must degrade to "changed" instead of throwing out of the
|
|
567
|
+
* setter that produced it.
|
|
568
|
+
*/
|
|
569
|
+
function hostsEqual(a, b) {
|
|
570
|
+
if (a === b)
|
|
571
|
+
return true;
|
|
572
|
+
if (!Array.isArray(a) || !Array.isArray(b))
|
|
573
|
+
return false;
|
|
574
|
+
return a.length === b.length && a.every((host, index) => {
|
|
575
|
+
const other = b[index];
|
|
576
|
+
return isPostureHost(host) && isPostureHost(other) && host.host === other.host && host.exposed === other.exposed;
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/** Whether a value has the shape one probed host is compared on. */
|
|
580
|
+
function isPostureHost(value) {
|
|
581
|
+
return typeof value === 'object' && value !== null;
|
|
582
|
+
}
|
|
540
583
|
/** Element-wise string list equality (interface order is meaningful). */
|
|
541
584
|
function sameStrings(a, b) {
|
|
542
585
|
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
@@ -70,7 +70,19 @@ export declare function pairedDeviceIdOf(req: IncomingMessage, service: PairingS
|
|
|
70
70
|
*/
|
|
71
71
|
export declare function makeRemoteApiRoutes(deps: RemoteApiDeps): WebRoute[];
|
|
72
72
|
/**
|
|
73
|
-
* Map one outer upgrade URL onto the loopback path
|
|
73
|
+
* Map one outer upgrade URL onto the loopback path, dropping the cookieless
|
|
74
|
+
* device credential on the way in.
|
|
75
|
+
*
|
|
76
|
+
* The outer handshake carries the device id as `?device=<id>` because WebSocket
|
|
77
|
+
* handshakes cannot carry headers from the Web API. That id is a live session
|
|
78
|
+
* credential — it opens the full host API through this same channel — and the
|
|
79
|
+
* inner leg is a request the rest of the loopback origin sees (on a WebSocket
|
|
80
|
+
* leg, a raw handshake request line written to a loopback socket), so forwarding
|
|
81
|
+
* it handed a working credential to every other observer of the local port for
|
|
82
|
+
* no benefit: the gate above has already authenticated the caller, and no
|
|
83
|
+
* upstream route reads the parameter (the gateway mux matches on path alone).
|
|
84
|
+
* The rest of the query rides through unchanged, so a caller's own parameters
|
|
85
|
+
* still reach the upstream.
|
|
74
86
|
*/
|
|
75
87
|
export declare function upgradeInnerPath(reqUrl: string | undefined, fallbackPath: string): string;
|
|
76
88
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,WAAW,CAAA;AAEhE,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAUhD,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,aAAa,EACb,oBAAoB,EACpB,2BAA2B,EAC3B,eAAe,GAChB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAgBvD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUhE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAQlG;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CAwCnE;AAED
|
|
1
|
+
{"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/remote-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,WAAW,CAAA;AAEhE,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAA;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAGlD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAUhD,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,aAAa,EACb,oBAAoB,EACpB,2BAA2B,EAC3B,eAAe,GAChB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAgBvD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB;AAWD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUhE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAQlG;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CAwCnE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAYzF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,GAAG,eAAe,EAAE,CAyCjF"}
|
package/lib/types/remote-api.js
CHANGED
|
@@ -137,7 +137,19 @@ export function makeRemoteApiRoutes(deps) {
|
|
|
137
137
|
return [{ kind: 'prefix', path: REMOTE_PREFIX, handler }];
|
|
138
138
|
}
|
|
139
139
|
/**
|
|
140
|
-
* Map one outer upgrade URL onto the loopback path
|
|
140
|
+
* Map one outer upgrade URL onto the loopback path, dropping the cookieless
|
|
141
|
+
* device credential on the way in.
|
|
142
|
+
*
|
|
143
|
+
* The outer handshake carries the device id as `?device=<id>` because WebSocket
|
|
144
|
+
* handshakes cannot carry headers from the Web API. That id is a live session
|
|
145
|
+
* credential — it opens the full host API through this same channel — and the
|
|
146
|
+
* inner leg is a request the rest of the loopback origin sees (on a WebSocket
|
|
147
|
+
* leg, a raw handshake request line written to a loopback socket), so forwarding
|
|
148
|
+
* it handed a working credential to every other observer of the local port for
|
|
149
|
+
* no benefit: the gate above has already authenticated the caller, and no
|
|
150
|
+
* upstream route reads the parameter (the gateway mux matches on path alone).
|
|
151
|
+
* The rest of the query rides through unchanged, so a caller's own parameters
|
|
152
|
+
* still reach the upstream.
|
|
141
153
|
*/
|
|
142
154
|
export function upgradeInnerPath(reqUrl, fallbackPath) {
|
|
143
155
|
if (reqUrl === undefined || reqUrl === '')
|
|
@@ -152,6 +164,7 @@ export function upgradeInnerPath(reqUrl, fallbackPath) {
|
|
|
152
164
|
const inner = innerPathOf(url.pathname);
|
|
153
165
|
if (inner === undefined)
|
|
154
166
|
return fallbackPath;
|
|
167
|
+
url.searchParams.delete(REMOTE_DEVICE_QUERY);
|
|
155
168
|
return `${inner}${url.search}`;
|
|
156
169
|
}
|
|
157
170
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
3
|
"description": "Scan-to-pair remote access for the dsh web GUI that shares one official interface: a QR beside the settings button pairs phones and PCs into the same Web GUI (a portrait-touch adaptation layer for phones, full desktop on PCs) through one-time tokens and revocable device sessions, with a LAN bind toggle, optional Cloudflare tunnel, and one-click family self-update",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.5",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"license": "Apache-2.0",
|
|
39
39
|
"dsh": {
|
|
40
40
|
"engines": {
|
|
41
|
-
"dsh": ">=0.
|
|
41
|
+
"dsh": ">=0.2.0-rc.2"
|
|
42
42
|
},
|
|
43
43
|
"bundle": {
|
|
44
44
|
"patch": "./cordis.patch.yml"
|
|
@@ -61,28 +61,28 @@
|
|
|
61
61
|
"zod": "^4.4.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@deepseek-ai/dsh": ">=0.
|
|
64
|
+
"@deepseek-ai/dsh": ">=0.2.0-rc.2",
|
|
65
65
|
"react": "^18.2.0",
|
|
66
66
|
"react-dom": "^18.2.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
70
|
-
"@deepseek-ai/dsh-agent": "^0.
|
|
71
|
-
"@deepseek-ai/dsh-api-remotes": "^0.
|
|
72
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.
|
|
73
|
-
"@deepseek-ai/dsh-api-workspace-controller": "^0.
|
|
74
|
-
"@deepseek-ai/dsh-client-connection": "^0.
|
|
75
|
-
"@deepseek-ai/dsh-client-locale": "^0.
|
|
76
|
-
"@deepseek-ai/dsh-client-store": "^0.
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.
|
|
78
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.
|
|
79
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.
|
|
80
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "^0.
|
|
81
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.
|
|
82
|
-
"@deepseek-ai/dsh-commands": "^0.
|
|
83
|
-
"@deepseek-ai/dsh-host-webserver": "^0.
|
|
84
|
-
"@deepseek-ai/dsh-invariants": "^0.
|
|
85
|
-
"@deepseek-ai/dsh-settings": "^0.
|
|
70
|
+
"@deepseek-ai/dsh-agent": "^0.2.0-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-api-remotes": "^0.2.0-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.2.0-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.2.0-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.0-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
|
|
76
|
+
"@deepseek-ai/dsh-client-store": "^0.2.0-rc.2",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.2",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.2",
|
|
79
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.2",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.2.0-rc.2",
|
|
81
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2",
|
|
82
|
+
"@deepseek-ai/dsh-commands": "^0.2.0-rc.2",
|
|
83
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.2",
|
|
84
|
+
"@deepseek-ai/dsh-invariants": "^0.2.0-rc.2",
|
|
85
|
+
"@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
|
|
86
86
|
"@testing-library/dom": "^10.4.1",
|
|
87
87
|
"@testing-library/react": "^16.3.2",
|
|
88
88
|
"@types/node": "^22.20.0",
|
|
@@ -41,6 +41,13 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
|
|
|
41
41
|
descriptionKey: TKey
|
|
42
42
|
/** Optional rich header description rendered instead of the plain t(descriptionKey) text (the plain text stays as the tooltip). */
|
|
43
43
|
descriptionNode?: ReactNode
|
|
44
|
+
/**
|
|
45
|
+
* Optional leading mark identifying the plugin. A card without one keeps the
|
|
46
|
+
* plain official header shape, so the chrome stays a readable sibling of the
|
|
47
|
+
* built-in PluginCard wherever a family plugin lands in the official bundle
|
|
48
|
+
* seat.
|
|
49
|
+
*/
|
|
50
|
+
icon?: ReactNode
|
|
44
51
|
/** The card's form state: availability, writability, and what a save would do. */
|
|
45
52
|
state: CardShell
|
|
46
53
|
/** Write every staged edit. */
|
|
@@ -91,6 +98,7 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
|
|
|
91
98
|
const header = alwaysOpen === true
|
|
92
99
|
? (
|
|
93
100
|
<div className={css.headerStatic}>
|
|
101
|
+
{props.icon !== undefined ? <span className={css.mark} aria-hidden="true">{props.icon}</span> : null}
|
|
94
102
|
<span className={css.headText}>
|
|
95
103
|
<span className={css.name} title={title}>{title}</span>
|
|
96
104
|
<span className={css.description} title={description}>{props.descriptionNode ?? description}</span>
|
|
@@ -106,6 +114,7 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
|
|
|
106
114
|
aria-label={`${props.t(open ? 'settings.collapse' : 'settings.expand')}: ${title}`}
|
|
107
115
|
onClick={() => { setOpen(!open) }}
|
|
108
116
|
>
|
|
117
|
+
{props.icon !== undefined ? <span className={css.mark} aria-hidden="true">{props.icon}</span> : null}
|
|
109
118
|
<span className={css.headText}>
|
|
110
119
|
<span className={css.name} title={title}>{title}</span>
|
|
111
120
|
<span className={css.description} title={description}>{props.descriptionNode ?? description}</span>
|
|
@@ -9,6 +9,7 @@ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-cli
|
|
|
9
9
|
import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
10
10
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
|
11
11
|
import { PluginSettingsCard, ValueField, BooleanField } from './PluginSettingsCard.tsx'
|
|
12
|
+
import { PhoneIcon } from './PhoneIcon.tsx'
|
|
12
13
|
import { CardForm, booleanField, numberField, secretField, textField, type CardActions, type CardShell, type FieldState as CardFieldState } from './settings-form.ts'
|
|
13
14
|
import { LanBindStatusError, readLanBindStatus, shouldStopLanBindPoll, type LanBindFrame } from './pair-api.ts'
|
|
14
15
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
@@ -173,6 +174,7 @@ export function RemoteSettingsCard(props: RemoteSettingsCardProps) {
|
|
|
173
174
|
t={t}
|
|
174
175
|
titleKey="settings.title"
|
|
175
176
|
descriptionKey="settings.description"
|
|
177
|
+
icon={<PhoneIcon size={18} />}
|
|
176
178
|
defaultOpen={false}
|
|
177
179
|
state={state}
|
|
178
180
|
onSave={props.save}
|
package/src/client/index.ts
CHANGED
|
@@ -242,9 +242,9 @@ export function apply(ctx: ClientContext): void {
|
|
|
242
242
|
|
|
243
243
|
const t = ctx.locale.bind(NS)
|
|
244
244
|
// Hand the `remote` translate seat to the module-scope adaptation layer:
|
|
245
|
-
// the whale
|
|
246
|
-
//
|
|
247
|
-
//
|
|
245
|
+
// the whale label was rendered with the English fallback before any
|
|
246
|
+
// dictionary existed; the wiring plus the layer's own sync tick re-renders
|
|
247
|
+
// it in the active locale.
|
|
248
248
|
if (adapt !== undefined) adapt.translate = t
|
|
249
249
|
// The settings form this card edits. The family group's binder resolves a
|
|
250
250
|
// family namespace to the profile entry that owns it and binds that entry's
|
package/src/client/locales.ts
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
export const zh = {
|
|
5
5
|
'entry.label': '远程访问',
|
|
6
6
|
'mobile.whale.open': '打开侧边栏',
|
|
7
|
-
'mobile.composer.pickModel': '选择模型',
|
|
8
|
-
'mobile.composer.pickEffort': '选择推理等级',
|
|
9
7
|
'title': '远程访问',
|
|
10
8
|
'subtitle': '通过手机或另一台电脑配对,远程使用同一份 Web 界面(官方界面 + 移动端适配)',
|
|
11
9
|
'card.title': '设备配对',
|
|
@@ -15,7 +13,7 @@ export const zh = {
|
|
|
15
13
|
'status.disconnected': '已配对设备离线',
|
|
16
14
|
'status.stopped': '已停止远程访问',
|
|
17
15
|
'status.lanRequired': '此功能需要局域网绑定或公网地址才能使用',
|
|
18
|
-
'status.lanRequiredHint': '当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请在本插件的设置卡片中打开“局域网访问”(装在全家桶里时在「设置 → Web 插件 →
|
|
16
|
+
'status.lanRequiredHint': '当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请在本插件的设置卡片中打开“局域网访问”(装在全家桶里时在「设置 → Web 插件 → 远程访问」,单独安装时在设置 → 插件 → 本插件行),或用 dsh web --host 0.0.0.0 重新启动,或填写内网穿透的公网地址。',
|
|
19
17
|
'status.loopbackRequired': '配对面板仅限本机使用',
|
|
20
18
|
'status.loopbackRequiredHint': '请通过 http://127.0.0.1 打开此页面后重试;手机请使用配对链接访问。',
|
|
21
19
|
'status.unreachable': '无法连接配对服务',
|
|
@@ -72,8 +70,8 @@ export const zh = {
|
|
|
72
70
|
'relay.failed': '固定域名同步失败:{error}。正在自动重试,期间二维码可能使用临时地址。',
|
|
73
71
|
'close.label': '关闭远程访问面板',
|
|
74
72
|
// 插件设置卡片(家族 list 席位,或官方 plugins.bundle.config 席位)。
|
|
75
|
-
'settings.title': '
|
|
76
|
-
'settings.description': '
|
|
73
|
+
'settings.title': '远程访问',
|
|
74
|
+
'settings.description': '手机配对、公网隧道与设备限额。',
|
|
77
75
|
'settings.enabled': '启用远程访问',
|
|
78
76
|
'settings.enabledHint': '关闭后移除侧边栏入口并停用配对路由与局域网栅栏。',
|
|
79
77
|
'settings.tokenTtlMs': '配对令牌有效期(毫秒)',
|
|
@@ -135,8 +133,6 @@ export type SettingsCardKey = RemoteKey
|
|
|
135
133
|
export const en = {
|
|
136
134
|
'entry.label': 'Remote access',
|
|
137
135
|
'mobile.whale.open': 'Open sidebar',
|
|
138
|
-
'mobile.composer.pickModel': 'Pick model',
|
|
139
|
-
'mobile.composer.pickEffort': 'Pick reasoning effort',
|
|
140
136
|
'title': 'Remote access',
|
|
141
137
|
'subtitle': 'Pair a phone or another computer to share the same Web GUI (official UI + mobile adaptation)',
|
|
142
138
|
'card.title': 'Pair a device',
|
|
@@ -203,8 +199,8 @@ export const en = {
|
|
|
203
199
|
'relay.failed': 'Stable-hostname sync failed: {error}. Retrying automatically; the QR may use the ephemeral address meanwhile.',
|
|
204
200
|
'close.label': 'Close remote access panel',
|
|
205
201
|
// Plugin settings card (the family list seat, or the official bundle-config seat).
|
|
206
|
-
'settings.title': 'Remote access
|
|
207
|
-
'settings.description': '
|
|
202
|
+
'settings.title': 'Remote access',
|
|
203
|
+
'settings.description': 'Phone pairing, public tunnel and device limits.',
|
|
208
204
|
'settings.enabled': 'Enable remote access',
|
|
209
205
|
'settings.enabledHint': 'When off, the sidebar entry is removed and pairing routes plus the LAN fence stop.',
|
|
210
206
|
'settings.tokenTtlMs': 'Pairing token lifetime (ms)',
|