@linxin666/dsh-remote-web-ui 0.3.4 → 0.3.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/lib/client.js +92 -3
- package/lib/client.js.map +1 -1
- package/lib/index.js +4 -0
- package/lib/types/client/FenceNotice.d.ts +12 -9
- package/lib/types/client/FenceNotice.d.ts.map +1 -1
- package/lib/types/client/FenceNotice.js +67 -2
- package/lib/types/client/locales.d.ts +12 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +12 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +4 -0
- package/package.json +1 -1
- package/src/client/FenceNotice.tsx +81 -1
- package/src/client/locales.ts +12 -0
- package/src/client/remote.module.css +72 -4
- package/src/index.ts +6 -0
package/lib/index.js
CHANGED
|
@@ -4087,6 +4087,10 @@ function applyImpl(ctx, config) {
|
|
|
4087
4087
|
if (!resolve().enabled) return false;
|
|
4088
4088
|
return isPairedDeviceRequest(service, request);
|
|
4089
4089
|
});
|
|
4090
|
+
if (lanAddresses.length > 0) {
|
|
4091
|
+
const urls = lanAddresses.map((ip) => `http://${ip}:${String(ctx.webServer.port)}/m/`).join(" , ");
|
|
4092
|
+
console.log(`remote-web-ui: mobile UI reachable on LAN at ${urls}`);
|
|
4093
|
+
}
|
|
4090
4094
|
const sync = () => {
|
|
4091
4095
|
const value = resolve();
|
|
4092
4096
|
service.config = pairingConfigOf(value);
|
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unpaired-desktop notice: a full-page blocking surface rendered when the remote channel
|
|
3
|
-
* (see remote-channel.ts) refuses a call because this desktop browser has no
|
|
4
|
-
* live paired-device cookie. Retires automatically once a gated call
|
|
5
|
-
* succeeds (the channel reports pairing) or when the channel itself is
|
|
6
|
-
* torn down (requirePairingForLan off / plugin disabled), so it never
|
|
7
|
-
* outlives the unpaired state it describes.
|
|
8
|
-
*/
|
|
9
1
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
|
|
10
2
|
/** Notice props: localized copy. */
|
|
11
3
|
export interface FenceNoticeProps {
|
|
12
4
|
t: TranslateNS<'remote'>;
|
|
13
5
|
/** Retry after the user has opened a freshly issued computer pairing link. */
|
|
14
6
|
onRetry: () => void;
|
|
7
|
+
/** Optional custom pairing acceptor for testing / dependency injection. */
|
|
8
|
+
onAccept?: (token: string) => Promise<{
|
|
9
|
+
ok: true;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
code: 'invalid' | 'used' | 'forbidden';
|
|
13
|
+
}>;
|
|
15
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Extract a pairing token from either a raw token string or a copied pairing link URL.
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractPairToken(input: string): string | undefined;
|
|
16
19
|
/**
|
|
17
20
|
* Render the unpaired blocking page.
|
|
18
21
|
* @param props - localized copy.
|
|
19
22
|
* @returns the notice element.
|
|
20
23
|
*/
|
|
21
|
-
export declare function FenceNotice({ t, onRetry }: FenceNoticeProps): import("react").JSX.Element;
|
|
24
|
+
export declare function FenceNotice({ t, onRetry, onAccept }: FenceNoticeProps): import("react").JSX.Element;
|
|
22
25
|
//# sourceMappingURL=FenceNotice.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FenceNotice.d.ts","sourceRoot":"","sources":["../../../src/client/FenceNotice.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FenceNotice.d.ts","sourceRoot":"","sources":["../../../src/client/FenceNotice.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAInE,oCAAoC;AACpC,MAAM,WAAW,gBAAgB;IAC/B,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACxB,8EAA8E;IAC9E,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,WAAW,CAAA;KAAE,CAAC,CAAA;CAC5G;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAWlE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,QAAqB,EAAE,EAAE,gBAAgB,+BAgFlF"}
|
|
@@ -1,10 +1,75 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Unpaired-desktop notice: a full-page blocking surface rendered when the remote channel
|
|
4
|
+
* (see remote-channel.ts) refuses a call because this desktop browser has no
|
|
5
|
+
* live paired-device cookie. Retires automatically once a gated call
|
|
6
|
+
* succeeds (the channel reports pairing) or when the channel itself is
|
|
7
|
+
* torn down (requirePairingForLan off / plugin disabled), so it never
|
|
8
|
+
* outlives the unpaired state it describes.
|
|
9
|
+
*/
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import { acceptPair } from "./pair-api.js";
|
|
2
12
|
import css from './remote.module.css';
|
|
13
|
+
/**
|
|
14
|
+
* Extract a pairing token from either a raw token string or a copied pairing link URL.
|
|
15
|
+
*/
|
|
16
|
+
export function extractPairToken(input) {
|
|
17
|
+
const trimmed = input.trim();
|
|
18
|
+
if (trimmed === '')
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
const url = new URL(trimmed);
|
|
22
|
+
const token = url.searchParams.get('pair');
|
|
23
|
+
if (token !== null && token !== '')
|
|
24
|
+
return token;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Plain token string
|
|
28
|
+
}
|
|
29
|
+
return trimmed;
|
|
30
|
+
}
|
|
3
31
|
/**
|
|
4
32
|
* Render the unpaired blocking page.
|
|
5
33
|
* @param props - localized copy.
|
|
6
34
|
* @returns the notice element.
|
|
7
35
|
*/
|
|
8
|
-
export function FenceNotice({ t, onRetry }) {
|
|
9
|
-
|
|
36
|
+
export function FenceNotice({ t, onRetry, onAccept = acceptPair }) {
|
|
37
|
+
const [tokenInput, setTokenInput] = useState('');
|
|
38
|
+
const [submitting, setSubmitting] = useState(false);
|
|
39
|
+
const [errorMsg, setErrorMsg] = useState(undefined);
|
|
40
|
+
const handleSubmit = async (event) => {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
const token = extractPairToken(tokenInput);
|
|
43
|
+
if (!token)
|
|
44
|
+
return;
|
|
45
|
+
setSubmitting(true);
|
|
46
|
+
setErrorMsg(undefined);
|
|
47
|
+
try {
|
|
48
|
+
const result = await onAccept(token);
|
|
49
|
+
if (result.ok) {
|
|
50
|
+
onRetry();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (result.code === 'invalid') {
|
|
54
|
+
setErrorMsg(t('fence.unpaired.tokenInvalid'));
|
|
55
|
+
}
|
|
56
|
+
else if (result.code === 'used') {
|
|
57
|
+
setErrorMsg(t('fence.unpaired.tokenUsed'));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
setErrorMsg(t('fence.unpaired.tokenFailed'));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
setErrorMsg(t('fence.unpaired.tokenFailed'));
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
setSubmitting(false);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return (_jsx("div", { className: css.fencePage, role: "dialog", "aria-modal": "true", "aria-labelledby": "remote-fence-title", children: _jsxs("main", { className: css.fenceCard, "data-dsh-plugin": "remote-web-ui", children: [_jsx("div", { className: css.fenceMark, "aria-hidden": "true", children: "\u00D7" }), _jsx("p", { className: css.fenceEyebrow, children: t('fence.unpaired.eyebrow') }), _jsx("h1", { id: "remote-fence-title", className: css.fenceTitle, children: t('fence.unpaired.title') }), _jsx("p", { className: css.fenceDetail, children: t('fence.unpaired.hint') }), _jsxs("ol", { className: css.fenceSteps, children: [_jsx("li", { children: t('fence.unpaired.stepDesktop') }), _jsx("li", { children: t('fence.unpaired.stepLink') }), _jsx("li", { children: t('fence.unpaired.stepOpen') })] }), _jsxs("form", { className: css.fenceForm, onSubmit: handleSubmit, children: [_jsxs("div", { className: css.fenceInputRow, children: [_jsx("input", { type: "text", className: css.fenceInput, placeholder: t('fence.unpaired.tokenPlaceholder'), value: tokenInput, onChange: event => {
|
|
71
|
+
setTokenInput(event.target.value);
|
|
72
|
+
if (errorMsg !== undefined)
|
|
73
|
+
setErrorMsg(undefined);
|
|
74
|
+
}, disabled: submitting, "aria-label": t('fence.unpaired.tokenPlaceholder') }), _jsx("button", { className: css.fencePairButton, type: "submit", disabled: submitting || tokenInput.trim() === '', children: submitting ? t('fence.unpaired.pairing') : t('fence.unpaired.pairAction') })] }), errorMsg !== undefined && (_jsx("p", { className: css.fenceError, role: "alert", children: errorMsg }))] }), _jsx("button", { className: css.fenceRetry, type: "button", onClick: onRetry, children: t('fence.unpaired.retry') }), _jsx("p", { className: css.fenceFootnote, children: t('fence.unpaired.footnote') })] }) }));
|
|
10
75
|
}
|
|
@@ -36,6 +36,12 @@ export declare const zh: {
|
|
|
36
36
|
'fence.unpaired.stepLink': string;
|
|
37
37
|
'fence.unpaired.stepOpen': string;
|
|
38
38
|
'fence.unpaired.retry': string;
|
|
39
|
+
'fence.unpaired.tokenPlaceholder': string;
|
|
40
|
+
'fence.unpaired.pairAction': string;
|
|
41
|
+
'fence.unpaired.pairing': string;
|
|
42
|
+
'fence.unpaired.tokenInvalid': string;
|
|
43
|
+
'fence.unpaired.tokenUsed': string;
|
|
44
|
+
'fence.unpaired.tokenFailed': string;
|
|
39
45
|
'fence.unpaired.footnote': string;
|
|
40
46
|
'posture.exposed': string;
|
|
41
47
|
'posture.exposedHint': string;
|
|
@@ -178,6 +184,12 @@ export declare const en: {
|
|
|
178
184
|
'fence.unpaired.stepLink': string;
|
|
179
185
|
'fence.unpaired.stepOpen': string;
|
|
180
186
|
'fence.unpaired.retry': string;
|
|
187
|
+
'fence.unpaired.tokenPlaceholder': string;
|
|
188
|
+
'fence.unpaired.pairAction': string;
|
|
189
|
+
'fence.unpaired.pairing': string;
|
|
190
|
+
'fence.unpaired.tokenInvalid': string;
|
|
191
|
+
'fence.unpaired.tokenUsed': string;
|
|
192
|
+
'fence.unpaired.tokenFailed': string;
|
|
181
193
|
'fence.unpaired.footnote': string;
|
|
182
194
|
'posture.exposed': string;
|
|
183
195
|
'posture.exposedHint': string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,mEAAmE;AACnE,eAAO,MAAM,EAAE
|
|
1
|
+
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgJmB,CAAA;AAElC,sCAAsC;AACtC,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,EAAE,CAAA;AAEvC,wDAAwD;AACxD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAA;AAEvC,mEAAmE;AACnE,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgJsB,CAAA"}
|
|
@@ -36,6 +36,12 @@ export const zh = {
|
|
|
36
36
|
'fence.unpaired.stepLink': '在“设备配对”中复制电脑配对链接。',
|
|
37
37
|
'fence.unpaired.stepOpen': '在当前浏览器打开该链接,完成授权后即可进入。',
|
|
38
38
|
'fence.unpaired.retry': '重新检测',
|
|
39
|
+
'fence.unpaired.tokenPlaceholder': '或在此直接粘贴配对链接 / Token',
|
|
40
|
+
'fence.unpaired.pairAction': '立即配对',
|
|
41
|
+
'fence.unpaired.pairing': '配对中…',
|
|
42
|
+
'fence.unpaired.tokenInvalid': '配对链接或 Token 无效已过期',
|
|
43
|
+
'fence.unpaired.tokenUsed': '该配对链接已被使用',
|
|
44
|
+
'fence.unpaired.tokenFailed': '配对失败,请检查网络或重新获取链接',
|
|
39
45
|
'fence.unpaired.footnote': '请勿使用他人提供的配对链接;管理员可随时取消此设备的授权。',
|
|
40
46
|
'posture.exposed': '/api 通道对未配对设备敞开',
|
|
41
47
|
'posture.exposedHint': '以下来源的请求未经配对即可访问完整桌面 API:{hosts}。请移除对应来源的 --trusted-host(配对机制已覆盖远程访问),或改为仅绑定 127.0.0.1 并使用隧道。',
|
|
@@ -176,6 +182,12 @@ export const en = {
|
|
|
176
182
|
'fence.unpaired.stepLink': 'Copy the computer pairing link under Pair a device.',
|
|
177
183
|
'fence.unpaired.stepOpen': 'Open that link in this browser. After authorization, the workspace will become available.',
|
|
178
184
|
'fence.unpaired.retry': 'Check again',
|
|
185
|
+
'fence.unpaired.tokenPlaceholder': 'Or paste pairing link / token here',
|
|
186
|
+
'fence.unpaired.pairAction': 'Pair Now',
|
|
187
|
+
'fence.unpaired.pairing': 'Pairing…',
|
|
188
|
+
'fence.unpaired.tokenInvalid': 'Pairing link or token is invalid or expired',
|
|
189
|
+
'fence.unpaired.tokenUsed': 'This pairing link has already been used',
|
|
190
|
+
'fence.unpaired.tokenFailed': 'Pairing failed, please check network or issue a new link',
|
|
179
191
|
'fence.unpaired.footnote': 'Do not use pairing links from people you do not trust. An administrator can revoke this device at any time.',
|
|
180
192
|
'posture.exposed': 'The /api channel is open to unpaired devices',
|
|
181
193
|
'posture.exposedHint': 'Requests from {hosts} reach the full desktop API without pairing. Remove --trusted-host for them (pairing already covers remote access), or bind loopback only and use the tunnel.',
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,CAAC,MAAM,aAAa,CAAA;AAI3B,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AA6BzF,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,MAAM;QACd;;;;;WAKG;QACH,UAAU,CACR,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GACrC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;KAC9B;CACF;AAED,iCAAiC;AACjC,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,+DAA+D;AAC/D,eAAO,MAAM,MAAM,UAAkD,CAAA;AAErE;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,uDAAqC,CAAA;AAElF,qEAAqE;AACrE,MAAM,WAAW,MAAM;IACrB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAY3B,CAAA;AAKF;;;;GAIG;AACH,KAAK,cAAc,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC,GAAG;IAC9E,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,IAAI,CAC5C,cAAc,EACd,YAAY,GAAG,gBAAgB,GAAG,YAAY,GAAG,cAAc,GAAG,YAAY,GAAG,aAAa,CAC/F,GAAG,aAAa,CAShB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,MAAkB,GAAG,MAAM,CAEnE;AAiBD;;;;GAIG;AACH,eAAO,MAAM,KAAK,kBAAuD,CAAA;AAEzE,iBAAS,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,CAAC,MAAM,aAAa,CAAA;AAI3B,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AA6BzF,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,MAAM;QACd;;;;;WAKG;QACH,UAAU,CACR,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,IAAI,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GACrC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;KAC9B;CACF;AAED,iCAAiC;AACjC,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAEnC,+DAA+D;AAC/D,eAAO,MAAM,MAAM,UAAkD,CAAA;AAErE;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,uDAAqC,CAAA;AAElF,qEAAqE;AACrE,MAAM,WAAW,MAAM;IACrB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,2EAA2E;IAC3E,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,eAAO,MAAM,MAAM,EAAE,CAAC,CAAC,MAAM,CAY3B,CAAA;AAKF;;;;GAIG;AACH,KAAK,cAAc,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC,GAAG;IAC9E,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,IAAI,CAC5C,cAAc,EACd,YAAY,GAAG,gBAAgB,GAAG,YAAY,GAAG,cAAc,GAAG,YAAY,GAAG,aAAa,CAC/F,GAAG,aAAa,CAShB;AAED,4EAA4E;AAC5E,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,MAAkB,GAAG,MAAM,CAEnE;AAiBD;;;;GAIG;AACH,eAAO,MAAM,KAAK,kBAAuD,CAAA;AAEzE,iBAAS,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAwVtD"}
|
package/lib/types/index.js
CHANGED
|
@@ -357,6 +357,10 @@ function applyImpl(ctx, config) {
|
|
|
357
357
|
return false;
|
|
358
358
|
return isPairedDeviceRequest(service, request);
|
|
359
359
|
});
|
|
360
|
+
if (lanAddresses.length > 0) {
|
|
361
|
+
const urls = lanAddresses.map(ip => `http://${ip}:${String(ctx.webServer.port)}/m/`).join(' , ');
|
|
362
|
+
console.log(`remote-web-ui: mobile UI reachable on LAN at ${urls}`);
|
|
363
|
+
}
|
|
360
364
|
const sync = () => {
|
|
361
365
|
const value = resolve();
|
|
362
366
|
service.config = pairingConfigOf(value);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
3
|
"description": "Scan-to-pair mobile and PC remote control for the dsh web GUI: QR entry beside the settings button, one-time pairing tokens, a gated /remote/api desktop channel, live device status, and revocable sessions",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* torn down (requirePairingForLan off / plugin disabled), so it never
|
|
7
7
|
* outlives the unpaired state it describes.
|
|
8
8
|
*/
|
|
9
|
+
import { useState, type FormEvent } from 'react'
|
|
9
10
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
11
|
+
import { acceptPair } from './pair-api.ts'
|
|
10
12
|
import css from './remote.module.css'
|
|
11
13
|
|
|
12
14
|
/** Notice props: localized copy. */
|
|
@@ -14,6 +16,24 @@ export interface FenceNoticeProps {
|
|
|
14
16
|
t: TranslateNS<'remote'>
|
|
15
17
|
/** Retry after the user has opened a freshly issued computer pairing link. */
|
|
16
18
|
onRetry: () => void
|
|
19
|
+
/** Optional custom pairing acceptor for testing / dependency injection. */
|
|
20
|
+
onAccept?: (token: string) => Promise<{ ok: true } | { ok: false; code: 'invalid' | 'used' | 'forbidden' }>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract a pairing token from either a raw token string or a copied pairing link URL.
|
|
25
|
+
*/
|
|
26
|
+
export function extractPairToken(input: string): string | undefined {
|
|
27
|
+
const trimmed = input.trim()
|
|
28
|
+
if (trimmed === '') return undefined
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(trimmed)
|
|
31
|
+
const token = url.searchParams.get('pair')
|
|
32
|
+
if (token !== null && token !== '') return token
|
|
33
|
+
} catch {
|
|
34
|
+
// Plain token string
|
|
35
|
+
}
|
|
36
|
+
return trimmed
|
|
17
37
|
}
|
|
18
38
|
|
|
19
39
|
/**
|
|
@@ -21,7 +41,39 @@ export interface FenceNoticeProps {
|
|
|
21
41
|
* @param props - localized copy.
|
|
22
42
|
* @returns the notice element.
|
|
23
43
|
*/
|
|
24
|
-
export function FenceNotice({ t, onRetry }: FenceNoticeProps) {
|
|
44
|
+
export function FenceNotice({ t, onRetry, onAccept = acceptPair }: FenceNoticeProps) {
|
|
45
|
+
const [tokenInput, setTokenInput] = useState('')
|
|
46
|
+
const [submitting, setSubmitting] = useState(false)
|
|
47
|
+
const [errorMsg, setErrorMsg] = useState<string | undefined>(undefined)
|
|
48
|
+
|
|
49
|
+
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
50
|
+
event.preventDefault()
|
|
51
|
+
const token = extractPairToken(tokenInput)
|
|
52
|
+
if (!token) return
|
|
53
|
+
|
|
54
|
+
setSubmitting(true)
|
|
55
|
+
setErrorMsg(undefined)
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const result = await onAccept(token)
|
|
59
|
+
if (result.ok) {
|
|
60
|
+
onRetry()
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
if (result.code === 'invalid') {
|
|
64
|
+
setErrorMsg(t('fence.unpaired.tokenInvalid'))
|
|
65
|
+
} else if (result.code === 'used') {
|
|
66
|
+
setErrorMsg(t('fence.unpaired.tokenUsed'))
|
|
67
|
+
} else {
|
|
68
|
+
setErrorMsg(t('fence.unpaired.tokenFailed'))
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
setErrorMsg(t('fence.unpaired.tokenFailed'))
|
|
72
|
+
} finally {
|
|
73
|
+
setSubmitting(false)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
25
77
|
return (
|
|
26
78
|
<div className={css.fencePage} role="dialog" aria-modal="true" aria-labelledby="remote-fence-title">
|
|
27
79
|
<main className={css.fenceCard} data-dsh-plugin="remote-web-ui">
|
|
@@ -34,6 +86,34 @@ export function FenceNotice({ t, onRetry }: FenceNoticeProps) {
|
|
|
34
86
|
<li>{t('fence.unpaired.stepLink')}</li>
|
|
35
87
|
<li>{t('fence.unpaired.stepOpen')}</li>
|
|
36
88
|
</ol>
|
|
89
|
+
|
|
90
|
+
<form className={css.fenceForm} onSubmit={handleSubmit}>
|
|
91
|
+
<div className={css.fenceInputRow}>
|
|
92
|
+
<input
|
|
93
|
+
type="text"
|
|
94
|
+
className={css.fenceInput}
|
|
95
|
+
placeholder={t('fence.unpaired.tokenPlaceholder')}
|
|
96
|
+
value={tokenInput}
|
|
97
|
+
onChange={event => {
|
|
98
|
+
setTokenInput(event.target.value)
|
|
99
|
+
if (errorMsg !== undefined) setErrorMsg(undefined)
|
|
100
|
+
}}
|
|
101
|
+
disabled={submitting}
|
|
102
|
+
aria-label={t('fence.unpaired.tokenPlaceholder')}
|
|
103
|
+
/>
|
|
104
|
+
<button
|
|
105
|
+
className={css.fencePairButton}
|
|
106
|
+
type="submit"
|
|
107
|
+
disabled={submitting || tokenInput.trim() === ''}
|
|
108
|
+
>
|
|
109
|
+
{submitting ? t('fence.unpaired.pairing') : t('fence.unpaired.pairAction')}
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
{errorMsg !== undefined && (
|
|
113
|
+
<p className={css.fenceError} role="alert">{errorMsg}</p>
|
|
114
|
+
)}
|
|
115
|
+
</form>
|
|
116
|
+
|
|
37
117
|
<button className={css.fenceRetry} type="button" onClick={onRetry}>
|
|
38
118
|
{t('fence.unpaired.retry')}
|
|
39
119
|
</button>
|
package/src/client/locales.ts
CHANGED
|
@@ -37,6 +37,12 @@ export const zh = {
|
|
|
37
37
|
'fence.unpaired.stepLink': '在“设备配对”中复制电脑配对链接。',
|
|
38
38
|
'fence.unpaired.stepOpen': '在当前浏览器打开该链接,完成授权后即可进入。',
|
|
39
39
|
'fence.unpaired.retry': '重新检测',
|
|
40
|
+
'fence.unpaired.tokenPlaceholder': '或在此直接粘贴配对链接 / Token',
|
|
41
|
+
'fence.unpaired.pairAction': '立即配对',
|
|
42
|
+
'fence.unpaired.pairing': '配对中…',
|
|
43
|
+
'fence.unpaired.tokenInvalid': '配对链接或 Token 无效已过期',
|
|
44
|
+
'fence.unpaired.tokenUsed': '该配对链接已被使用',
|
|
45
|
+
'fence.unpaired.tokenFailed': '配对失败,请检查网络或重新获取链接',
|
|
40
46
|
'fence.unpaired.footnote': '请勿使用他人提供的配对链接;管理员可随时取消此设备的授权。',
|
|
41
47
|
'posture.exposed': '/api 通道对未配对设备敞开',
|
|
42
48
|
'posture.exposedHint': '以下来源的请求未经配对即可访问完整桌面 API:{hosts}。请移除对应来源的 --trusted-host(配对机制已覆盖远程访问),或改为仅绑定 127.0.0.1 并使用隧道。',
|
|
@@ -184,6 +190,12 @@ export const en = {
|
|
|
184
190
|
'fence.unpaired.stepLink': 'Copy the computer pairing link under Pair a device.',
|
|
185
191
|
'fence.unpaired.stepOpen': 'Open that link in this browser. After authorization, the workspace will become available.',
|
|
186
192
|
'fence.unpaired.retry': 'Check again',
|
|
193
|
+
'fence.unpaired.tokenPlaceholder': 'Or paste pairing link / token here',
|
|
194
|
+
'fence.unpaired.pairAction': 'Pair Now',
|
|
195
|
+
'fence.unpaired.pairing': 'Pairing…',
|
|
196
|
+
'fence.unpaired.tokenInvalid': 'Pairing link or token is invalid or expired',
|
|
197
|
+
'fence.unpaired.tokenUsed': 'This pairing link has already been used',
|
|
198
|
+
'fence.unpaired.tokenFailed': 'Pairing failed, please check network or issue a new link',
|
|
187
199
|
'fence.unpaired.footnote': 'Do not use pairing links from people you do not trust. An administrator can revoke this device at any time.',
|
|
188
200
|
'posture.exposed': 'The /api channel is open to unpaired devices',
|
|
189
201
|
'posture.exposedHint': 'Requests from {hosts} reach the full desktop API without pairing. Remove --trusted-host for them (pairing already covers remote access), or bind loopback only and use the tunnel.',
|
|
@@ -484,23 +484,91 @@
|
|
|
484
484
|
margin-top: 8px;
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
.
|
|
487
|
+
.fenceForm {
|
|
488
|
+
margin-top: 20px;
|
|
488
489
|
width: 100%;
|
|
489
|
-
|
|
490
|
-
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.fenceInputRow {
|
|
493
|
+
display: flex;
|
|
494
|
+
gap: 8px;
|
|
495
|
+
width: 100%;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.fenceInput {
|
|
499
|
+
flex: 1;
|
|
500
|
+
min-width: 0;
|
|
501
|
+
padding: 10px 14px;
|
|
502
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
503
|
+
border-radius: 10px;
|
|
504
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
505
|
+
color: var(--dsw-alias-label-primary);
|
|
506
|
+
font: inherit;
|
|
507
|
+
font-size: 13px;
|
|
508
|
+
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.fenceInput:focus {
|
|
512
|
+
outline: none;
|
|
513
|
+
border-color: var(--dsw-alias-brand-primary);
|
|
514
|
+
box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.15);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.fencePairButton {
|
|
518
|
+
flex-shrink: 0;
|
|
519
|
+
padding: 10px 18px;
|
|
491
520
|
border: 1px solid var(--dsw-alias-brand-primary);
|
|
492
521
|
border-radius: 10px;
|
|
493
522
|
background: var(--dsw-alias-brand-primary);
|
|
494
523
|
color: #fff;
|
|
495
524
|
font: inherit;
|
|
525
|
+
font-size: 13px;
|
|
496
526
|
font-weight: 500;
|
|
497
527
|
cursor: pointer;
|
|
528
|
+
transition: filter 120ms ease, opacity 120ms ease;
|
|
498
529
|
}
|
|
499
530
|
|
|
500
|
-
.
|
|
531
|
+
.fencePairButton:hover:not(:disabled) {
|
|
501
532
|
filter: brightness(0.96);
|
|
502
533
|
}
|
|
503
534
|
|
|
535
|
+
.fencePairButton:disabled {
|
|
536
|
+
opacity: 0.55;
|
|
537
|
+
cursor: not-allowed;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.fencePairButton:focus-visible {
|
|
541
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
542
|
+
outline-offset: 2px;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.fenceError {
|
|
546
|
+
margin: 10px 0 0;
|
|
547
|
+
color: var(--dsw-alias-state-error-primary);
|
|
548
|
+
font-size: 13px;
|
|
549
|
+
line-height: 1.4;
|
|
550
|
+
text-align: left;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.fenceRetry {
|
|
554
|
+
width: 100%;
|
|
555
|
+
margin-top: 12px;
|
|
556
|
+
padding: 10px 16px;
|
|
557
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
558
|
+
border-radius: 10px;
|
|
559
|
+
background: transparent;
|
|
560
|
+
color: var(--dsw-alias-label-secondary);
|
|
561
|
+
font: inherit;
|
|
562
|
+
font-weight: 500;
|
|
563
|
+
cursor: pointer;
|
|
564
|
+
transition: background-color 120ms ease, color 120ms ease;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.fenceRetry:hover {
|
|
568
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
569
|
+
color: var(--dsw-alias-label-primary);
|
|
570
|
+
}
|
|
571
|
+
|
|
504
572
|
.fenceRetry:focus-visible {
|
|
505
573
|
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
506
574
|
outline-offset: 3px;
|
package/src/index.ts
CHANGED
|
@@ -462,6 +462,12 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
462
462
|
if (!resolve().enabled) return false
|
|
463
463
|
return isPairedDeviceRequest(service, request)
|
|
464
464
|
})
|
|
465
|
+
|
|
466
|
+
if (lanAddresses.length > 0) {
|
|
467
|
+
const urls = lanAddresses.map(ip => `http://${ip}:${String(ctx.webServer.port)}/m/`).join(' , ')
|
|
468
|
+
console.log(`remote-web-ui: mobile UI reachable on LAN at ${urls}`)
|
|
469
|
+
}
|
|
470
|
+
|
|
465
471
|
const sync = (): void => {
|
|
466
472
|
const value = resolve()
|
|
467
473
|
service.config = pairingConfigOf(value)
|