@linxin666/dsh-remote-web-ui 0.3.24 → 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.i18n.yaml +2 -2
- package/README.md +13 -10
- package/README.zh.md +13 -10
- package/icon.svg +3 -0
- package/lib/client.js +263 -129
- package/lib/client.js.map +1 -1
- package/lib/index.js +565 -256
- package/lib/types/client/FenceNotice.d.ts.map +1 -1
- package/lib/types/client/FenceNotice.js +0 -3
- package/lib/types/client/PluginSettingsCard.d.ts +4 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +3 -2
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +14 -9
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +13 -4
- package/lib/types/client/RemoteSettingsCard.d.ts +6 -6
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +14 -8
- package/lib/types/client/UpdateEntry.js +2 -2
- package/lib/types/client/UpdatePanel.js +2 -2
- package/lib/types/client/deep-link.js +2 -2
- package/lib/types/client/index.d.ts +16 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +47 -18
- package/lib/types/client/locales.d.ts +0 -2
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +6 -8
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +103 -22
- package/lib/types/client/pair-api.d.ts +32 -3
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +44 -6
- package/lib/types/client/plugin-card-seat.d.ts +23 -16
- package/lib/types/client/plugin-card-seat.d.ts.map +1 -1
- package/lib/types/client/plugin-card-seat.js +16 -14
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +18 -5
- package/lib/types/client/settings-form.d.ts +17 -17
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +30 -27
- package/lib/types/http.d.ts +18 -1
- package/lib/types/http.d.ts.map +1 -1
- package/lib/types/http.js +22 -1
- package/lib/types/index.d.ts +19 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +91 -76
- package/lib/types/inner-auth.d.ts +3 -2
- package/lib/types/inner-auth.d.ts.map +1 -1
- package/lib/types/inner-auth.js +3 -2
- package/lib/types/pair-grant.d.ts +66 -0
- package/lib/types/pair-grant.d.ts.map +1 -0
- package/lib/types/pair-grant.js +95 -0
- package/lib/types/pairing.d.ts +44 -12
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +69 -20
- package/lib/types/pairing.persist.test.js +26 -0
- package/lib/types/posture.d.ts +11 -3
- package/lib/types/posture.d.ts.map +1 -1
- package/lib/types/posture.js +8 -3
- package/lib/types/remote-api.d.ts +10 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +34 -34
- package/lib/types/remote-channel-boot.d.ts +6 -1
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +16 -8
- package/lib/types/remote-channel-rules.d.ts +14 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +8 -0
- package/lib/types/remote-methods.d.ts +1 -1
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +1 -0
- package/lib/types/routes.d.ts +47 -5
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +160 -58
- package/lib/types/tunnel.d.ts +21 -6
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +58 -11
- package/lib/types/update.d.ts +4 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +74 -26
- package/package.json +23 -20
- package/src/client/FenceNotice.tsx +0 -2
- package/src/client/PluginSettingsCard.tsx +7 -2
- package/src/client/RemoteEntry.tsx +14 -9
- package/src/client/RemotePanel.tsx +17 -7
- package/src/client/RemoteSettingsCard.tsx +16 -9
- package/src/client/UpdateEntry.tsx +2 -2
- package/src/client/UpdatePanel.tsx +3 -3
- package/src/client/deep-link.ts +2 -2
- package/src/client/index.ts +61 -27
- package/src/client/locales.ts +6 -8
- package/src/client/mobile-adapt.ts +97 -20
- package/src/client/pair-api.ts +47 -7
- package/src/client/plugin-card-seat.ts +24 -17
- package/src/client/remote-channel.ts +19 -4
- package/src/client/remote.module.css +38 -0
- package/src/client/settings-form.ts +34 -31
- package/src/http.ts +23 -1
- package/src/index.ts +102 -79
- package/src/inner-auth.ts +3 -2
- package/src/pair-grant.ts +131 -0
- package/src/pairing.persist.test.ts +36 -0
- package/src/pairing.ts +75 -24
- package/src/posture.ts +12 -4
- package/src/remote-api.ts +34 -41
- package/src/remote-channel-boot.ts +16 -8
- package/src/remote-channel-rules.ts +17 -0
- package/src/remote-methods.ts +1 -0
- package/src/routes.ts +165 -57
- package/src/tunnel.ts +56 -11
- package/src/update.ts +74 -24
package/lib/types/update.d.ts
CHANGED
|
@@ -219,7 +219,11 @@ export interface UpdateRunDeps {
|
|
|
219
219
|
timeoutMs?: number;
|
|
220
220
|
/** Platform override (defaults to process.platform; test seam). */
|
|
221
221
|
platform?: NodeJS.Platform;
|
|
222
|
+
/** Process-group kill seam (defaults to process.kill; test seam). */
|
|
223
|
+
killImpl?: KillImpl;
|
|
222
224
|
}
|
|
225
|
+
/** Seam for the process-group kill (tests inject a recorder). */
|
|
226
|
+
export type KillImpl = (pid: number, signal: NodeJS.Signals) => void;
|
|
223
227
|
/**
|
|
224
228
|
* Run the update inside the profile directory. Tries pnpm first, falls back
|
|
225
229
|
* to corepack and then npx when the previous command is missing (ENOENT);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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"}
|
package/lib/types/update.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
import { readFileSync } from 'node:fs';
|
|
12
12
|
import { dirname, join } from 'node:path';
|
|
13
13
|
import { spawn } from 'node:child_process';
|
|
14
|
-
import {
|
|
14
|
+
import { createOutputCapture } from "./console-output.js";
|
|
15
|
+
import { withIdentityEncoding } from "./http.js";
|
|
15
16
|
/** npm registry base used for version probes. */
|
|
16
17
|
export const NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
17
18
|
/** The family scope every dsh-web package is published under. */
|
|
@@ -230,7 +231,7 @@ export async function fetchLatestVersion(name, fetchImpl, timeoutMs = 10_000) {
|
|
|
230
231
|
const controller = new AbortController();
|
|
231
232
|
const timer = setTimeout(() => { controller.abort(); }, timeoutMs);
|
|
232
233
|
try {
|
|
233
|
-
const response = await fetchImpl(NPM_REGISTRY + '/' + name.replace('/', '%2F') + '/latest', { signal: controller.signal });
|
|
234
|
+
const response = await fetchImpl(NPM_REGISTRY + '/' + name.replace('/', '%2F') + '/latest', withIdentityEncoding({ signal: controller.signal }));
|
|
234
235
|
if (!response.ok)
|
|
235
236
|
return undefined;
|
|
236
237
|
const body = await response.json();
|
|
@@ -313,10 +314,10 @@ export async function fetchGitHubReleaseNotes(version, fetchImpl = fetch, timeou
|
|
|
313
314
|
const controller = new AbortController();
|
|
314
315
|
const timer = setTimeout(() => { controller.abort(); }, timeoutMs);
|
|
315
316
|
try {
|
|
316
|
-
const response = await fetchImpl(`https://api.github.com/repos/${UPDATE_RELEASE_REPO}/releases/tags/v${version}`, {
|
|
317
|
+
const response = await fetchImpl(`https://api.github.com/repos/${UPDATE_RELEASE_REPO}/releases/tags/v${version}`, withIdentityEncoding({
|
|
317
318
|
headers: { accept: 'application/vnd.github+json', 'user-agent': 'dsh-web-update' },
|
|
318
319
|
signal: controller.signal,
|
|
319
|
-
});
|
|
320
|
+
}));
|
|
320
321
|
if (!response.ok)
|
|
321
322
|
return undefined;
|
|
322
323
|
const body = await response.json();
|
|
@@ -425,6 +426,13 @@ export async function checkUpdates(deps) {
|
|
|
425
426
|
}
|
|
426
427
|
/** Cap on captured pnpm output (keeps error payloads bounded). */
|
|
427
428
|
const OUTPUT_CAP = 16 * 1024;
|
|
429
|
+
/**
|
|
430
|
+
* Characters a package name may contain before it is handed to the update
|
|
431
|
+
* command line. Deliberately looser than the strict family shape (the tests
|
|
432
|
+
* and future callers use short names) but it admits no whitespace, quotes,
|
|
433
|
+
* shell metacharacters, or path separators beyond the scope slash.
|
|
434
|
+
*/
|
|
435
|
+
const SHELL_SAFE_PACKAGE_RE = /^@?[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
428
436
|
/**
|
|
429
437
|
* Windows cmd command-not-found stderr. With shell:true a missing shim
|
|
430
438
|
* exits with code 1 or 9009 (cmd cannot report ENOENT), so the fallback chain
|
|
@@ -433,14 +441,14 @@ const OUTPUT_CAP = 16 * 1024;
|
|
|
433
441
|
*/
|
|
434
442
|
const WIN_CMD_MISSING_RE = /not recognized as an internal or external command|不是内部或外部命令/i;
|
|
435
443
|
/**
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
* accumulator — decoding per \`data\` event would split multi-byte characters.
|
|
444
|
+
* Fold one decoded candidate output into the running total under the display
|
|
445
|
+
* cap (the tail carries the failure reason).
|
|
446
|
+
* @param output - the accumulated text.
|
|
447
|
+
* @param text - the candidate's decoded text.
|
|
441
448
|
*/
|
|
442
|
-
function
|
|
443
|
-
|
|
449
|
+
function capOutput(output, text) {
|
|
450
|
+
const combined = output + text;
|
|
451
|
+
return combined.length > OUTPUT_CAP ? combined.slice(combined.length - OUTPUT_CAP) : combined;
|
|
444
452
|
}
|
|
445
453
|
/**
|
|
446
454
|
* Bypass for pnpm 11's supply-chain gate: `minimumReleaseAge` (default 24 h)
|
|
@@ -462,16 +470,24 @@ const MIN_RELEASE_AGE_OVERRIDE = '--config.minimumReleaseAge=0';
|
|
|
462
470
|
export function runUpdate(deps) {
|
|
463
471
|
return new Promise((resolve) => {
|
|
464
472
|
const spawnImpl = deps.spawnImpl ?? spawn;
|
|
465
|
-
const packages = deps.packages;
|
|
466
473
|
const platform = deps.platform ?? process.platform;
|
|
474
|
+
// The names come from the profile/anchor manifests and the win32 candidate
|
|
475
|
+
// runs through a shell, so anything outside the npm name shape is dropped
|
|
476
|
+
// instead of being concatenated into a command line (a dependency key
|
|
477
|
+
// containing shell metacharacters would otherwise run as a second command).
|
|
478
|
+
const packages = deps.packages.filter(name => SHELL_SAFE_PACKAGE_RE.test(name));
|
|
467
479
|
// Windows ships pnpm/corepack/npx as .cmd shims; Node's spawn cannot
|
|
468
480
|
// start .cmd files directly (ENOENT even when installed), so route
|
|
469
481
|
// them through cmd.exe there. POSIX spawns stay shell-free.
|
|
482
|
+
// POSIX: the child leads its own process group (detached) so the timeout
|
|
483
|
+
// can signal pnpm's whole tree — pnpm spawns lifecycle-script children that
|
|
484
|
+
// a direct SIGTERM to the wrapper leaves running against the profile.
|
|
470
485
|
const spawnOptions = {
|
|
471
486
|
cwd: deps.profileDir,
|
|
472
487
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
473
|
-
...(platform === 'win32' ? { shell: true } : {}),
|
|
488
|
+
...(platform === 'win32' ? { shell: true } : { detached: true }),
|
|
474
489
|
};
|
|
490
|
+
const killImpl = deps.killImpl ?? process.kill;
|
|
475
491
|
// Ordered fallback chain: each is tried only when the previous one is
|
|
476
492
|
// missing on PATH (ENOENT on the spawn error event, never on close).
|
|
477
493
|
// `--latest` is required: dsh plugin writes exact-version specs (e.g.
|
|
@@ -492,16 +508,22 @@ export function runUpdate(deps) {
|
|
|
492
508
|
// (its tail is subject to OUTPUT_CAP). The win32 missing-command test must
|
|
493
509
|
// run against `currentOutput` so a previous candidate's 'not recognized'
|
|
494
510
|
// stderr cannot misclassify a real failure in the next one.
|
|
511
|
+
// Each candidate accumulates raw bytes and is decoded exactly once
|
|
512
|
+
// (console-output.ts): decoding per `data` event splits a multi-byte
|
|
513
|
+
// character that straddles two reads into replacement characters, which is
|
|
514
|
+
// what the panel then shows as garbled pnpm diagnostics.
|
|
495
515
|
let output = '';
|
|
496
|
-
let
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if (
|
|
504
|
-
|
|
516
|
+
let capture = createOutputCapture(OUTPUT_CAP);
|
|
517
|
+
let candidateFolded = false;
|
|
518
|
+
const append = (chunk) => { capture.push(chunk); };
|
|
519
|
+
/** This candidate's decoded text (idempotent). */
|
|
520
|
+
const candidateText = () => capture.read();
|
|
521
|
+
/** Fold this candidate's text into the running output exactly once. */
|
|
522
|
+
const foldCandidate = () => {
|
|
523
|
+
if (candidateFolded)
|
|
524
|
+
return;
|
|
525
|
+
candidateFolded = true;
|
|
526
|
+
output = capOutput(output, capture.read());
|
|
505
527
|
};
|
|
506
528
|
let currentChild;
|
|
507
529
|
// Terminal guard: once a result is produced the promise is settled and no
|
|
@@ -524,7 +546,11 @@ export function runUpdate(deps) {
|
|
|
524
546
|
const pid = currentChild?.pid;
|
|
525
547
|
if (pid !== undefined && pid > 0) {
|
|
526
548
|
try {
|
|
527
|
-
|
|
549
|
+
// A spawn failure arrives as an asynchronous 'error' event, which
|
|
550
|
+
// the try/catch cannot see; an emitter with no error listener throws
|
|
551
|
+
// and would take the host process down on this path.
|
|
552
|
+
const killer = spawnImpl('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore' });
|
|
553
|
+
killer.on?.('error', () => { });
|
|
528
554
|
}
|
|
529
555
|
catch {
|
|
530
556
|
// Best-effort kill; fall through to the timeout result.
|
|
@@ -532,8 +558,26 @@ export function runUpdate(deps) {
|
|
|
532
558
|
}
|
|
533
559
|
}
|
|
534
560
|
else {
|
|
535
|
-
|
|
561
|
+
// Detached child: its process-group id is its pid, so the negative pid
|
|
562
|
+
// reaches pnpm's grandchildren too. A group kill throws when the child
|
|
563
|
+
// already exited; fall back to the direct kill.
|
|
564
|
+
const pid = currentChild?.pid;
|
|
565
|
+
if (pid !== undefined && pid > 0) {
|
|
566
|
+
try {
|
|
567
|
+
killImpl(-pid, 'SIGTERM');
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
try {
|
|
571
|
+
currentChild?.kill('SIGTERM');
|
|
572
|
+
}
|
|
573
|
+
catch { /* already gone */ }
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
currentChild?.kill('SIGTERM');
|
|
578
|
+
}
|
|
536
579
|
}
|
|
580
|
+
foldCandidate();
|
|
537
581
|
finish({ ok: false, exitCode: null, output, error: 'update timed out; install process killed', errorCode: 'timeout' });
|
|
538
582
|
}, deps.timeoutMs ?? 10 * 60_000);
|
|
539
583
|
const runCandidate = (index) => {
|
|
@@ -550,7 +594,8 @@ export function runUpdate(deps) {
|
|
|
550
594
|
return;
|
|
551
595
|
}
|
|
552
596
|
const candidate = candidates[index];
|
|
553
|
-
|
|
597
|
+
capture = createOutputCapture(OUTPUT_CAP);
|
|
598
|
+
candidateFolded = false;
|
|
554
599
|
const child = spawnImpl(candidate.command, candidate.args, spawnOptions);
|
|
555
600
|
currentChild = child;
|
|
556
601
|
let settled = false;
|
|
@@ -564,6 +609,7 @@ export function runUpdate(deps) {
|
|
|
564
609
|
child.stderr?.on('data', append);
|
|
565
610
|
child.on('error', (error) => {
|
|
566
611
|
once(() => {
|
|
612
|
+
foldCandidate();
|
|
567
613
|
if (error.code === 'ENOENT') {
|
|
568
614
|
// Command missing: try the next candidate in the chain.
|
|
569
615
|
runCandidate(index + 1);
|
|
@@ -580,11 +626,13 @@ export function runUpdate(deps) {
|
|
|
580
626
|
// 'not recognized' or localized message instead of ENOENT — keep the
|
|
581
627
|
// chain going. Checked against this candidate's own output so a prior
|
|
582
628
|
// fallback's stderr cannot misclassify a real failure here.
|
|
583
|
-
if (platform === 'win32' && (code === 9009 || (code !== 0 && WIN_CMD_MISSING_RE.test(
|
|
629
|
+
if (platform === 'win32' && (code === 9009 || (code !== 0 && WIN_CMD_MISSING_RE.test(candidateText())))) {
|
|
630
|
+
foldCandidate();
|
|
584
631
|
runCandidate(index + 1);
|
|
585
632
|
return;
|
|
586
633
|
}
|
|
587
634
|
settled = true;
|
|
635
|
+
foldCandidate();
|
|
588
636
|
finish({
|
|
589
637
|
ok: code === 0,
|
|
590
638
|
exitCode: code,
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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
|
+
"version": "0.4.1",
|
|
5
|
+
"icon": "icon.svg",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"engines": {
|
|
7
8
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -31,12 +32,13 @@
|
|
|
31
32
|
"lib/**/*.d.ts.map",
|
|
32
33
|
"src",
|
|
33
34
|
"assets",
|
|
34
|
-
"cordis.patch.yml"
|
|
35
|
+
"cordis.patch.yml",
|
|
36
|
+
"icon.svg"
|
|
35
37
|
],
|
|
36
38
|
"license": "Apache-2.0",
|
|
37
39
|
"dsh": {
|
|
38
40
|
"engines": {
|
|
39
|
-
"dsh": ">=0.1.
|
|
41
|
+
"dsh": ">=0.1.7-rc.1"
|
|
40
42
|
},
|
|
41
43
|
"bundle": {
|
|
42
44
|
"patch": "./cordis.patch.yml"
|
|
@@ -58,27 +60,28 @@
|
|
|
58
60
|
"zod": "^4.4.3"
|
|
59
61
|
},
|
|
60
62
|
"peerDependencies": {
|
|
63
|
+
"@deepseek-ai/dsh": ">=0.1.7-rc.1",
|
|
61
64
|
"react": "^18.2.0",
|
|
62
65
|
"react-dom": "^18.2.0"
|
|
63
66
|
},
|
|
64
67
|
"devDependencies": {
|
|
65
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
66
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
67
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.
|
|
68
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.1.
|
|
69
|
-
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.
|
|
70
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.
|
|
71
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
72
|
-
"@deepseek-ai/dsh-client-store": "^0.1.
|
|
73
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.
|
|
74
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.
|
|
75
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.
|
|
76
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.
|
|
78
|
-
"@deepseek-ai/dsh-commands": "^0.1.
|
|
79
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.
|
|
80
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
81
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
68
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
69
|
+
"@deepseek-ai/dsh-agent": "^0.1.7-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.7-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.7-rc.1",
|
|
72
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.7-rc.1",
|
|
73
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.7-rc.1",
|
|
74
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.7-rc.1",
|
|
75
|
+
"@deepseek-ai/dsh-client-store": "^0.1.7-rc.1",
|
|
76
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.7-rc.1",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.1",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.1",
|
|
79
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.7-rc.1",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.1",
|
|
81
|
+
"@deepseek-ai/dsh-commands": "^0.1.7-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-invariants": "^0.1.7-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-settings": "^0.1.7-rc.1",
|
|
82
85
|
"@testing-library/dom": "^10.4.1",
|
|
83
86
|
"@testing-library/react": "^16.3.2",
|
|
84
87
|
"@types/node": "^22.20.0",
|
|
@@ -62,8 +62,6 @@ export function FenceNotice({ t, onRetry, onAccept = acceptPair }: FenceNoticePr
|
|
|
62
62
|
}
|
|
63
63
|
if (result.code === 'invalid') {
|
|
64
64
|
setErrorMsg(t('fence.unpaired.tokenInvalid'))
|
|
65
|
-
} else if (result.code === 'used') {
|
|
66
|
-
setErrorMsg(t('fence.unpaired.tokenUsed'))
|
|
67
65
|
} else {
|
|
68
66
|
setErrorMsg(t('fence.unpaired.tokenFailed'))
|
|
69
67
|
}
|
|
@@ -64,6 +64,10 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
|
|
|
64
64
|
* edits, so the footer would sit there permanently disabled.
|
|
65
65
|
*/
|
|
66
66
|
hideFooter?: boolean
|
|
67
|
+
/** Render children even when the settings namespace is not exposed by the host. */
|
|
68
|
+
renderChildrenWhenNotExposed?: boolean
|
|
69
|
+
/** Hide the 'not exposed' warning banner when the settings namespace is not exposed. */
|
|
70
|
+
hideNotExposedNotice?: boolean
|
|
67
71
|
/** The plugin's controls. */
|
|
68
72
|
children: ReactNode
|
|
69
73
|
}
|
|
@@ -126,14 +130,15 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
|
|
|
126
130
|
// official settings allowlist omits third-party namespaces): show a card
|
|
127
131
|
// that explains the gap instead of vanishing, so a missing card never
|
|
128
132
|
// reads as a missing plugin.
|
|
129
|
-
if (!state.exposed) {
|
|
133
|
+
if (!state.exposed && props.renderChildrenWhenNotExposed !== true) {
|
|
134
|
+
const showNotice = props.hideNotExposedNotice !== true
|
|
130
135
|
return (
|
|
131
136
|
<li className={cardClass}>
|
|
132
137
|
{header}
|
|
133
138
|
{expanded
|
|
134
139
|
? (
|
|
135
140
|
<div className={css.body}>
|
|
136
|
-
<p className={css.notExposed} role="status">{props.t('settings.notExposed')}</p>
|
|
141
|
+
{showNotice ? <p className={css.notExposed} role="status">{props.t('settings.notExposed')}</p> : null}
|
|
137
142
|
</div>
|
|
138
143
|
)
|
|
139
144
|
: null}
|
|
@@ -181,18 +181,23 @@ export function RemoteEntry({ wide, t }: RemoteEntryProps) {
|
|
|
181
181
|
useEffect(() => closeEventSource, [closeEventSource])
|
|
182
182
|
|
|
183
183
|
const handleStop = useCallback(() => {
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
//
|
|
188
|
-
|
|
184
|
+
// The phase flip lands only once the server accepted the stop. A refused
|
|
185
|
+
// stop changes nothing server-side and the service emits no state change
|
|
186
|
+
// (notify dedupes identical snapshots), so an optimistic flip would stick
|
|
187
|
+
// and claim a revocation that never happened.
|
|
188
|
+
void stopPair().then(() => {
|
|
189
|
+
setState(previous => previous.kind === 'ready' ? { ...previous, phase: 'stopped' as PairingPhase, devices: [] } : previous)
|
|
190
|
+
}).catch(() => {})
|
|
189
191
|
}, [])
|
|
190
192
|
|
|
191
193
|
const handleRevoke = useCallback((deviceId: string) => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
// Same rule as stop: a refused unpair must leave the row, or the panel
|
|
195
|
+
// claims a still-live session is gone.
|
|
196
|
+
void revokePair(deviceId).then(() => {
|
|
197
|
+
setState(previous => previous.kind === 'ready'
|
|
198
|
+
? { ...previous, devices: previous.devices.filter(device => device.id !== deviceId) }
|
|
199
|
+
: previous)
|
|
200
|
+
}).catch(() => {})
|
|
196
201
|
}, [])
|
|
197
202
|
|
|
198
203
|
const handleRefresh = useCallback(() => {
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* actions arrive through props from the entry's behavior component.
|
|
6
6
|
*/
|
|
7
7
|
import clsx from 'clsx'
|
|
8
|
+
import { memo } from 'react'
|
|
8
9
|
import { QRCodeSVG } from 'qrcode.react'
|
|
9
10
|
import {
|
|
10
|
-
|
|
11
|
+
IconCloseOutlineRegular, IconCopyOutlineRegular, IconRefreshOutlineRegular, IconStopFillRegular,
|
|
11
12
|
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
12
13
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
13
14
|
import type { PairingPhase } from '../pairing.ts'
|
|
@@ -22,6 +23,15 @@ import {
|
|
|
22
23
|
import { deviceNameFromUserAgent } from './device-name.ts'
|
|
23
24
|
import css from './remote.module.css'
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* The QR symbol, memoized on the link: the panel re-renders on every SSE state
|
|
28
|
+
* frame (a paired phone heartbeats every 10s), and qrcode.react rebuilds the
|
|
29
|
+
* SVG path and element tree on each render.
|
|
30
|
+
*/
|
|
31
|
+
const PairQrCode = memo(function PairQrCode({ url, className }: { url: string; className: string }) {
|
|
32
|
+
return <QRCodeSVG value={url} size={184} level="M" marginSize={1} className={className} />
|
|
33
|
+
})
|
|
34
|
+
|
|
25
35
|
/** The panel's view state, owned by the entry component. */
|
|
26
36
|
export type PanelState =
|
|
27
37
|
| { kind: 'lan-required'; tunnel?: TunnelStatusFrame }
|
|
@@ -115,7 +125,7 @@ export function RemotePanel({
|
|
|
115
125
|
<p className={css.subtitle}>{t('subtitle')}</p>
|
|
116
126
|
</div>
|
|
117
127
|
<button type="button" className={css.close} aria-label={t('close.label')} onClick={onClose}>
|
|
118
|
-
<
|
|
128
|
+
<IconCloseOutlineRegular size={14} />
|
|
119
129
|
</button>
|
|
120
130
|
</div>
|
|
121
131
|
|
|
@@ -155,7 +165,7 @@ export function RemotePanel({
|
|
|
155
165
|
</span>
|
|
156
166
|
</div>
|
|
157
167
|
<div className={css.qrWrap} data-testid="remote-qr">
|
|
158
|
-
<
|
|
168
|
+
<PairQrCode url={state.url} className={css.qr} />
|
|
159
169
|
</div>
|
|
160
170
|
{state.expired
|
|
161
171
|
? <p className={css.expired}>{t('pair.expired')}</p>
|
|
@@ -170,7 +180,7 @@ export function RemotePanel({
|
|
|
170
180
|
<code className={css.link} title={state.url}>{state.url}</code>
|
|
171
181
|
</div>
|
|
172
182
|
<button type="button" className={css.copyLink} onClick={() => onCopy(state.url)}>
|
|
173
|
-
<
|
|
183
|
+
<IconCopyOutlineRegular size={14} />
|
|
174
184
|
{copied ? t('action.copied') : t('action.copyLink')}
|
|
175
185
|
</button>
|
|
176
186
|
</div>
|
|
@@ -185,7 +195,7 @@ export function RemotePanel({
|
|
|
185
195
|
className={css.copyLink}
|
|
186
196
|
onClick={() => onCopyToken ? onCopyToken(state.token!) : onCopy(state.token!)}
|
|
187
197
|
>
|
|
188
|
-
<
|
|
198
|
+
<IconCopyOutlineRegular size={14} />
|
|
189
199
|
{copiedToken ? t('action.copiedToken') : t('action.copyToken')}
|
|
190
200
|
</button>
|
|
191
201
|
</div>
|
|
@@ -246,11 +256,11 @@ export function RemotePanel({
|
|
|
246
256
|
|
|
247
257
|
<div className={css.actions}>
|
|
248
258
|
<button type="button" className={css.action} onClick={onStop}>
|
|
249
|
-
<
|
|
259
|
+
<IconStopFillRegular size={14} />
|
|
250
260
|
{t('action.stop')}
|
|
251
261
|
</button>
|
|
252
262
|
<button type="button" className={css.action} onClick={onRefresh}>
|
|
253
|
-
<
|
|
263
|
+
<IconRefreshOutlineRegular size={14} />
|
|
254
264
|
{t('action.refresh')}
|
|
255
265
|
</button>
|
|
256
266
|
</div>
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The remote-control settings card: pairing security and device limits.
|
|
3
3
|
* Registers into the `web-ui.plugin.item` child slot the Web UI plugin group
|
|
4
|
-
* renders, bound to the `remote-web-ui`
|
|
4
|
+
* renders, bound to the `remote-web-ui` profile entry's configuration form.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useEffect, useState } from 'react'
|
|
8
8
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
9
|
-
import type {
|
|
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
12
|
import { CardForm, booleanField, numberField, secretField, textField, type CardActions, type CardShell, type FieldState as CardFieldState } from './settings-form.ts'
|
|
13
|
-
import { readLanBindStatus, type LanBindFrame } from './pair-api.ts'
|
|
13
|
+
import { LanBindStatusError, readLanBindStatus, shouldStopLanBindPoll, type LanBindFrame } from './pair-api.ts'
|
|
14
14
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
15
15
|
|
|
16
16
|
/** The remote-control fields this card edits (the namespace's full schema). */
|
|
@@ -88,13 +88,13 @@ export interface RemoteSettingsCardFace extends CardActions {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
/** Bridges the `remote-web-ui`
|
|
91
|
+
/** Bridges the `remote-web-ui` form onto the card's staged form. */
|
|
92
92
|
export class RemoteSettingsCardController {
|
|
93
93
|
private readonly form: CardForm<RemoteSettings>
|
|
94
94
|
private readonly store: SnapshotStore<RemoteSettingsCardState>
|
|
95
95
|
|
|
96
|
-
/** @param scope - the
|
|
97
|
-
constructor(scope:
|
|
96
|
+
/** @param scope - the configuration form for the `remote-web-ui` entry. */
|
|
97
|
+
constructor(scope: ConfigForm<RemoteSettings>) {
|
|
98
98
|
this.form = new CardForm(scope, [
|
|
99
99
|
booleanField('enabled'),
|
|
100
100
|
numberField('tokenTtlMs'),
|
|
@@ -139,7 +139,7 @@ export class RemoteSettingsCardController {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
|
-
* Release the card's
|
|
142
|
+
* Release the card's form subscription and bound stores; the slot
|
|
143
143
|
* disposer calls this on teardown.
|
|
144
144
|
*/
|
|
145
145
|
dispose(): void {
|
|
@@ -175,6 +175,8 @@ export function RemoteSettingsCard(props: RemoteSettingsCardProps) {
|
|
|
175
175
|
descriptionKey="settings.description"
|
|
176
176
|
defaultOpen={false}
|
|
177
177
|
state={state}
|
|
178
|
+
renderChildrenWhenNotExposed
|
|
179
|
+
hideNotExposedNotice
|
|
178
180
|
onSave={props.save}
|
|
179
181
|
onDiscard={props.discard}
|
|
180
182
|
>
|
|
@@ -325,10 +327,15 @@ function LanBindStatus({ t }: { t: TranslateNS<'remote'> }) {
|
|
|
325
327
|
const read = (): void => {
|
|
326
328
|
void readLanBindStatus().then((value) => {
|
|
327
329
|
if (alive) setFrame(value)
|
|
328
|
-
}).catch(() => {
|
|
330
|
+
}).catch((error: unknown) => {
|
|
331
|
+
// The endpoint is loopback-only: a 401/403 means this origin (a paired
|
|
332
|
+
// phone) can never read it, so the 10s poll stops instead of retrying a
|
|
333
|
+
// known refusal for as long as the card stays expanded.
|
|
334
|
+
if (error instanceof LanBindStatusError && shouldStopLanBindPoll(error.status)) window.clearInterval(timer)
|
|
335
|
+
})
|
|
329
336
|
}
|
|
330
|
-
read()
|
|
331
337
|
const timer = window.setInterval(read, 10_000)
|
|
338
|
+
read()
|
|
332
339
|
return () => {
|
|
333
340
|
alive = false
|
|
334
341
|
window.clearInterval(timer)
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { useCallback, useEffect, useRef, useState } from "react"
|
|
10
10
|
import { createPortal } from "react-dom"
|
|
11
11
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
12
|
-
import {
|
|
12
|
+
import { IconDownloadOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
13
13
|
import type { UpdateStatus } from '../update.ts'
|
|
14
14
|
import { fetchUpdateStatus, runUpdate, UpdateStatusError } from "./update-api.ts"
|
|
15
15
|
import { UpdatePanel, type UpdateView } from "./UpdatePanel.tsx"
|
|
@@ -128,7 +128,7 @@ export function UpdateEntry({ wide, t }: UpdateEntryProps) {
|
|
|
128
128
|
title={updateLabel}
|
|
129
129
|
onClick={openPanel}
|
|
130
130
|
>
|
|
131
|
-
<
|
|
131
|
+
<IconDownloadOutlineRegular size={wide ? 16 : 18} />
|
|
132
132
|
{updateAvailable && wide && (
|
|
133
133
|
<span className={css.updateBadgeText}>{t("update.badge")}</span>
|
|
134
134
|
)}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* through props from the entry behavior component.
|
|
6
6
|
*/
|
|
7
7
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
8
|
-
import {
|
|
8
|
+
import { IconCloseOutlineRegular, IconRefreshOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
9
9
|
import type { UpdateReleaseNotes, UpdateRunResult, UpdateStatus } from '../update.ts'
|
|
10
10
|
import css from "./remote.module.css"
|
|
11
11
|
|
|
@@ -55,7 +55,7 @@ export function UpdatePanel({ t, view, onClose, onRecheck, onStartUpdate }: Upda
|
|
|
55
55
|
{subtitle !== undefined && <p className={css.subtitle}>{subtitle}</p>}
|
|
56
56
|
</div>
|
|
57
57
|
<button type="button" className={css.close} aria-label={t("update.close")} onClick={onClose}>
|
|
58
|
-
<
|
|
58
|
+
<IconCloseOutlineRegular />
|
|
59
59
|
</button>
|
|
60
60
|
</div>
|
|
61
61
|
{view.kind === "checking" && <p className={css.updateStatus}>{t("update.checking")}</p>}
|
|
@@ -86,7 +86,7 @@ export function UpdatePanel({ t, view, onClose, onRecheck, onStartUpdate }: Upda
|
|
|
86
86
|
{(view.kind === "done" || view.kind === "error") && (
|
|
87
87
|
<div className={css.updateActions}>
|
|
88
88
|
<button type="button" className={css.updateRetry} onClick={onRecheck}>
|
|
89
|
-
<
|
|
89
|
+
<IconRefreshOutlineRegular /> {t("update.retry")}
|
|
90
90
|
</button>
|
|
91
91
|
</div>
|
|
92
92
|
)}
|
package/src/client/deep-link.ts
CHANGED
|
@@ -70,8 +70,8 @@ async function runAccept(token: string, page: PageSurface): Promise<void> {
|
|
|
70
70
|
} catch {
|
|
71
71
|
sessionStorage.setItem(PAIR_FAILED_MARKER, 'failed')
|
|
72
72
|
}
|
|
73
|
-
// Drop the token from the URL either way:
|
|
74
|
-
//
|
|
73
|
+
// Drop the token from the URL either way: it is spent for this tab (the
|
|
74
|
+
// session is established), and a failed one must not loop.
|
|
75
75
|
const url = new URL(page.href)
|
|
76
76
|
url.searchParams.delete('pair')
|
|
77
77
|
page.replaceState(`${url.pathname}${url.search}${url.hash}`)
|