@linxin666/dsh-remote-web-ui 0.1.19 → 0.2.0
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 +41 -19
- package/README.zh.md +17 -12
- package/assets/mobile-icon-192.png +0 -0
- package/assets/mobile-icon-512.png +0 -0
- package/assets/mobile-service-worker.js +64 -0
- package/lib/client.js +57 -13
- package/lib/client.js.map +1 -1
- package/lib/index.js +408 -130
- package/lib/mobile.js +296 -22
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/FooterRemoteEntry.js +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +7 -1
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +5 -3
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +43 -13
- package/lib/types/client/RemotePanel.d.ts +1 -0
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +3 -3
- package/lib/types/client/UpdateEntry.d.ts.map +1 -1
- package/lib/types/client/UpdateEntry.js +37 -4
- package/lib/types/client/UpdatePanel.js +1 -1
- package/lib/types/client/deep-link.js +2 -2
- package/lib/types/client/index.js +7 -7
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +2 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/update-api.d.ts.map +1 -1
- package/lib/types/gate.d.ts +9 -0
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +16 -5
- package/lib/types/http.d.ts +14 -0
- package/lib/types/http.d.ts.map +1 -0
- package/lib/types/http.js +27 -0
- package/lib/types/index.d.ts +28 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +41 -22
- package/lib/types/invariant.d.ts.map +1 -1
- package/lib/types/mobile/PairRequiredView.d.ts +7 -0
- package/lib/types/mobile/PairRequiredView.d.ts.map +1 -0
- package/lib/types/mobile/PairRequiredView.js +28 -0
- package/lib/types/mobile/PairRequiredView.test.d.ts +2 -0
- package/lib/types/mobile/PairRequiredView.test.d.ts.map +1 -0
- package/lib/types/mobile/PairRequiredView.test.js +24 -0
- package/lib/types/mobile/api.js +1 -1
- package/lib/types/mobile/index.js +17 -4
- package/lib/types/mobile/markdown.d.ts +6 -4
- package/lib/types/mobile/markdown.d.ts.map +1 -1
- package/lib/types/mobile/markdown.js +8 -4
- package/lib/types/mobile/markdown.test.js +5 -1
- package/lib/types/mobile/messages.d.ts.map +1 -1
- package/lib/types/mobile/messages.js +14 -1
- package/lib/types/mobile/messages.test.js +1 -1
- package/lib/types/mobile/mobile-styles.d.ts +1 -1
- package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
- package/lib/types/mobile/mobile-styles.js +48 -0
- package/lib/types/mobile/mobile-theme.test.js +2 -2
- package/lib/types/mobile/mux.d.ts.map +1 -1
- package/lib/types/mobile/mux.js +1 -1
- package/lib/types/mobile/mux.test.js +1 -1
- package/lib/types/mobile/pairing.d.ts +31 -0
- package/lib/types/mobile/pairing.d.ts.map +1 -0
- package/lib/types/mobile/pairing.js +62 -0
- package/lib/types/mobile/rpc.d.ts.map +1 -1
- package/lib/types/mobile/theme-toggle.js +1 -1
- package/lib/types/mobile/views/App.d.ts +12 -16
- package/lib/types/mobile/views/App.d.ts.map +1 -1
- package/lib/types/mobile/views/App.js +39 -12
- package/lib/types/mobile/views/App.test.d.ts +2 -0
- package/lib/types/mobile/views/App.test.d.ts.map +1 -0
- package/lib/types/mobile/views/App.test.js +52 -0
- package/lib/types/mobile/views/ChatView.js +6 -6
- package/lib/types/mobile/views/ChatView.test.js +3 -3
- package/lib/types/mobile/views/SessionListView.js +3 -3
- package/lib/types/mobile/views/SessionListView.test.js +2 -2
- package/lib/types/mobile/views/WorkspaceView.js +3 -3
- package/lib/types/mobile/views/WorkspaceView.test.js +3 -3
- package/lib/types/mobile-api.d.ts +1 -1
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +11 -18
- package/lib/types/mobile-pwa.d.ts +6 -0
- package/lib/types/mobile-pwa.d.ts.map +1 -0
- package/lib/types/mobile-pwa.js +17 -0
- package/lib/types/mobile-routes.d.ts +5 -8
- package/lib/types/mobile-routes.d.ts.map +1 -1
- package/lib/types/mobile-routes.js +130 -39
- package/lib/types/mount-once.d.ts +25 -0
- package/lib/types/mount-once.d.ts.map +1 -0
- package/lib/types/mount-once.js +42 -0
- package/lib/types/pairing-access.d.ts +28 -0
- package/lib/types/pairing-access.d.ts.map +1 -0
- package/lib/types/pairing-access.js +26 -0
- package/lib/types/pairing.d.ts +27 -0
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +68 -0
- package/lib/types/pairing.persist.test.d.ts +2 -0
- package/lib/types/pairing.persist.test.d.ts.map +1 -0
- package/lib/types/pairing.persist.test.js +128 -0
- package/lib/types/routes.d.ts +6 -6
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +15 -20
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/update-routes.d.ts +2 -2
- package/lib/types/update-routes.d.ts.map +1 -1
- package/lib/types/update-routes.js +1 -6
- package/lib/types/update.d.ts +3 -3
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +40 -14
- package/package.json +12 -12
- package/src/client/PluginSettingsCard.tsx +10 -0
- package/src/client/RemoteEntry.tsx +40 -8
- package/src/client/RemotePanel.tsx +1 -1
- package/src/client/RemoteSettingsCard.tsx +1 -0
- package/src/client/UpdateEntry.tsx +36 -4
- package/src/client/deep-link.ts +1 -1
- package/src/client/locales.ts +2 -0
- package/src/client/remote.module.css +26 -0
- package/src/gate.ts +15 -3
- package/src/http.ts +30 -0
- package/src/index.ts +50 -16
- package/src/mobile/PairRequiredView.test.tsx +28 -0
- package/src/mobile/PairRequiredView.tsx +58 -0
- package/src/mobile/index.tsx +15 -1
- package/src/mobile/markdown.test.ts +5 -0
- package/src/mobile/markdown.ts +7 -4
- package/src/mobile/messages.ts +14 -1
- package/src/mobile/mobile-styles.ts +48 -0
- package/src/mobile/pairing.ts +77 -0
- package/src/mobile/views/App.test.tsx +62 -0
- package/src/mobile/views/App.tsx +53 -18
- package/src/mobile-api.ts +9 -17
- package/src/mobile-pwa.ts +19 -0
- package/src/mobile-routes.ts +137 -38
- package/src/mount-once.ts +48 -0
- package/src/pairing-access.ts +36 -0
- package/src/pairing.persist.test.ts +145 -0
- package/src/pairing.ts +70 -1
- package/src/routes.ts +12 -18
- package/src/update-routes.ts +1 -7
- package/src/update.ts +46 -15
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../../src/mobile/pairing.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAE3F,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAElC,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAIrD,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAgB/E;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,mFAAmF;AACnF,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,SAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAc3G;AAED,wFAAwF;AACxF,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,SAAiB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAejH"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const MOBILE_ROOT = '/m/';
|
|
2
|
+
/** Parse a pairing token or a copied pairing link without following its origin. */
|
|
3
|
+
export function parseMobilePairInput(value) {
|
|
4
|
+
const trimmed = value.trim();
|
|
5
|
+
if (trimmed === '')
|
|
6
|
+
return undefined;
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(trimmed);
|
|
9
|
+
const token = url.searchParams.get('pair');
|
|
10
|
+
if (token === null || token === '')
|
|
11
|
+
return undefined;
|
|
12
|
+
const workspaceId = url.searchParams.get('workspace');
|
|
13
|
+
return {
|
|
14
|
+
token,
|
|
15
|
+
...(workspaceId !== null && workspaceId !== '' ? { workspaceId } : {}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return { token: trimmed };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Build the safe, token-free mobile destination after successful pairing. */
|
|
23
|
+
export function mobilePairPath(workspaceId) {
|
|
24
|
+
return workspaceId === undefined ? MOBILE_ROOT : MOBILE_ROOT + '?workspace=' + encodeURIComponent(workspaceId);
|
|
25
|
+
}
|
|
26
|
+
/** Accept one pairing token on this exact browser or installed-web-app context. */
|
|
27
|
+
export async function acceptMobilePair(token, fetcher = fetch) {
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetcher('/api/pair/accept', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'content-type': 'application/json' },
|
|
32
|
+
body: JSON.stringify({ token }),
|
|
33
|
+
});
|
|
34
|
+
if (response.ok)
|
|
35
|
+
return { ok: true };
|
|
36
|
+
if (response.status === 404)
|
|
37
|
+
return { ok: false, message: '配对链接无效或已过期。' };
|
|
38
|
+
if (response.status === 409)
|
|
39
|
+
return { ok: false, message: '配对链接已被使用。' };
|
|
40
|
+
return { ok: false, message: '此设备无法使用该配对链接。' };
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { ok: false, message: '无法连接到配对服务。' };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Consume a QR pairing token before the mobile application starts making RPC calls. */
|
|
47
|
+
export async function consumeMobilePairUrl(href, fetcher = fetch) {
|
|
48
|
+
let url;
|
|
49
|
+
try {
|
|
50
|
+
url = new URL(href);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { kind: 'none' };
|
|
54
|
+
}
|
|
55
|
+
const token = url.searchParams.get('pair');
|
|
56
|
+
if (token === null || token === '')
|
|
57
|
+
return { kind: 'none' };
|
|
58
|
+
const workspaceId = url.searchParams.get('workspace');
|
|
59
|
+
const path = mobilePairPath(workspaceId === null || workspaceId === '' ? undefined : workspaceId);
|
|
60
|
+
const result = await acceptMobilePair(token, fetcher);
|
|
61
|
+
return result.ok ? { kind: 'accepted', path } : { kind: 'failed', path, message: result.message };
|
|
62
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../../../src/mobile/rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,0EAA0E;AAC1E,qBAAa,iBAAkB,SAAQ,KAAK;
|
|
1
|
+
{"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../../../src/mobile/rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,0EAA0E;AAC1E,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED,kEAAkE;AAClE,qBAAa,YAAa,SAAQ,KAAK;IACrC,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IAEjD,YAAY,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAInD;CACF;AAID,8EAA8E;AAC9E,wBAAgB,SAAS,IAAI,MAAM,CAMlC;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,CAAC,CAAC,CAmCZ"}
|
|
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* mode); the choice persists across visits.
|
|
7
7
|
*/
|
|
8
8
|
import { useSyncExternalStore } from 'react';
|
|
9
|
-
import { getMobileTheme, subscribeMobileTheme, toggleMobileTheme } from
|
|
9
|
+
import { getMobileTheme, subscribeMobileTheme, toggleMobileTheme } from './mobile-theme.js';
|
|
10
10
|
/** Sun glyph: circle + eight rays (inline SVG, no emoji). */
|
|
11
11
|
function SunIcon() {
|
|
12
12
|
return (_jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", "aria-hidden": true, children: [_jsx("circle", { cx: "12", cy: "12", r: "4.2" }), _jsx("path", { d: "M12 2.5v2.4M12 19.1v2.4M2.5 12h2.4M19.1 12h2.4M5 5l1.7 1.7M17.3 17.3 19 19M19 5l-1.7 1.7M6.7 17.3 5 19" })] }));
|
|
@@ -14,17 +14,6 @@ export interface SessionView {
|
|
|
14
14
|
running: boolean;
|
|
15
15
|
blank: boolean;
|
|
16
16
|
}
|
|
17
|
-
/** One rendered chat message (produced by the fold). */
|
|
18
|
-
export interface RenderMessage {
|
|
19
|
-
id: string;
|
|
20
|
-
kind: 'user' | 'assistant';
|
|
21
|
-
text: string;
|
|
22
|
-
seq: number;
|
|
23
|
-
time: number;
|
|
24
|
-
pending?: boolean;
|
|
25
|
-
failed?: boolean;
|
|
26
|
-
toolSummary?: string;
|
|
27
|
-
}
|
|
28
17
|
/** Read the optional workspace target carried from the pairing QR flow. */
|
|
29
18
|
export declare function mobileWorkspaceTarget(search: string): string | undefined;
|
|
30
19
|
/** Map a list row to the surface model; the title comes from projections when present. */
|
|
@@ -40,11 +29,18 @@ export declare function toSessionView(item: {
|
|
|
40
29
|
}): SessionView;
|
|
41
30
|
/** Human clock, e.g. "14:05" or "昨天 20:31". */
|
|
42
31
|
export declare function formatTime(epochMs: number): string;
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
32
|
+
/** Props accepted by the mobile root before it begins paired-device RPC calls. */
|
|
33
|
+
export interface AppProps {
|
|
34
|
+
initialPairError?: string;
|
|
35
|
+
}
|
|
36
|
+
/** Whether the mobile gateway rejected this browser for lack of a paired cookie. */
|
|
37
|
+
export declare function isUnpairedMobileError(error: unknown): boolean;
|
|
38
|
+
/** The result of probing the paired-device-only mobile preference endpoint. */
|
|
39
|
+
export type MobilePairState = 'checking' | 'paired' | 'unpaired' | 'unavailable';
|
|
40
|
+
/** Classify an initial mobile preference failure without treating outage as authorization. */
|
|
41
|
+
export declare function mobilePairStateForError(error: unknown): Extract<MobilePairState, 'unpaired' | 'unavailable'>;
|
|
42
|
+
/** Gate the independent mobile bundle until its own browser context is paired. */
|
|
43
|
+
export declare function App({ initialPairError }: AppProps): import("react").JSX.Element;
|
|
48
44
|
/** Shared error text for the surface's small failure affordances. */
|
|
49
45
|
export declare function errorText(error: unknown): string;
|
|
50
46
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/App.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,
|
|
1
|
+
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/App.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAmD,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAcjH,yDAAyD;AACzD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf;AAED,2EAA2E;AAC3E,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGxE;AAED,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAClC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAA;CACnD,GAAG,WAAW,CAad;AAED,+CAA+C;AAC/C,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CASlD;AAED,kFAAkF;AAClF,MAAM,WAAW,QAAQ;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE7D;AAED,+EAA+E;AAC/E,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,QAAQ,GAAG,UAAU,GAAG,aAAa,CAAA;AAEhF,8FAA8F;AAC9F,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,EAAE,UAAU,GAAG,aAAa,CAAC,CAE5G;AAED,kFAAkF;AAClF,wBAAgB,GAAG,CAAC,EAAE,gBAAgB,EAAE,EAAE,QAAQ,+BA4BjD;AAiED,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAKhD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIjE;AAED,6FAA6F;AAC7F,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,4CAEtF;AAED,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,CAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Mobile surface root: the view state machine (workspaces → sessions →
|
|
4
4
|
* chat) and the top-level data flows. Deliberately plain React state — no
|
|
@@ -6,12 +6,13 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
6
6
|
* affordance, and every piece of data is fetched on demand.
|
|
7
7
|
*/
|
|
8
8
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
9
|
-
import { history as fetchHistory, listSessions, listWorkspaces, prompt } from
|
|
10
|
-
import { MuxClient } from
|
|
11
|
-
import { RpcCallError, RpcTransportError } from
|
|
12
|
-
import { ChatView } from
|
|
13
|
-
import { SessionListView } from
|
|
14
|
-
import { WorkspaceView as WorkspaceRoster } from
|
|
9
|
+
import { fetchMobilePreferences, history as fetchHistory, listSessions, listWorkspaces, prompt } from '../api.js';
|
|
10
|
+
import { MuxClient } from '../mux.js';
|
|
11
|
+
import { RpcCallError, RpcTransportError } from '../rpc.js';
|
|
12
|
+
import { ChatView } from './ChatView.js';
|
|
13
|
+
import { SessionListView } from './SessionListView.js';
|
|
14
|
+
import { WorkspaceView as WorkspaceRoster } from './WorkspaceView.js';
|
|
15
|
+
import { PairRequiredView } from '../PairRequiredView.js';
|
|
15
16
|
/** Read the optional workspace target carried from the pairing QR flow. */
|
|
16
17
|
export function mobileWorkspaceTarget(search) {
|
|
17
18
|
const value = new URLSearchParams(search).get('workspace');
|
|
@@ -45,11 +46,37 @@ export function formatTime(epochMs) {
|
|
|
45
46
|
return `昨天 ${clock}`;
|
|
46
47
|
return `${String(date.getMonth() + 1)}月${String(date.getDate())}日 ${clock}`;
|
|
47
48
|
}
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
/** Whether the mobile gateway rejected this browser for lack of a paired cookie. */
|
|
50
|
+
export function isUnpairedMobileError(error) {
|
|
51
|
+
return error instanceof RpcTransportError && error.message === 'HTTP 403';
|
|
52
|
+
}
|
|
53
|
+
/** Classify an initial mobile preference failure without treating outage as authorization. */
|
|
54
|
+
export function mobilePairStateForError(error) {
|
|
55
|
+
return isUnpairedMobileError(error) ? 'unpaired' : 'unavailable';
|
|
56
|
+
}
|
|
57
|
+
/** Gate the independent mobile bundle until its own browser context is paired. */
|
|
58
|
+
export function App({ initialPairError }) {
|
|
59
|
+
const [pairState, setPairState] = useState('checking');
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
let current = true;
|
|
62
|
+
void fetchMobilePreferences().then(() => { if (current)
|
|
63
|
+
setPairState('paired'); }, (error) => { if (current)
|
|
64
|
+
setPairState(mobilePairStateForError(error)); });
|
|
65
|
+
return () => { current = false; };
|
|
66
|
+
}, []);
|
|
67
|
+
if (pairState === 'checking') {
|
|
68
|
+
return _jsx("main", { className: "mobile mobile-empty", children: _jsx("p", { className: "mobile-muted", children: "\u6B63\u5728\u8FDE\u63A5..." }) });
|
|
69
|
+
}
|
|
70
|
+
if (pairState === 'unpaired') {
|
|
71
|
+
return _jsx(PairRequiredView, { initialError: initialPairError, onPaired: (path) => { window.location.replace(path); } });
|
|
72
|
+
}
|
|
73
|
+
if (pairState === 'unavailable') {
|
|
74
|
+
return (_jsxs("main", { className: "mobile mobile-empty", children: [_jsx("p", { className: "mobile-error", role: "alert", children: "\u65E0\u6CD5\u8FDE\u63A5\u5230\u8FD0\u884C\u4E2D\u7684 DSH host\u3002" }), _jsx("button", { className: "mobile-new", type: "button", onClick: () => { window.location.reload(); }, children: "\u91CD\u8BD5" })] }));
|
|
75
|
+
}
|
|
76
|
+
return _jsx(PairedApp, {});
|
|
77
|
+
}
|
|
78
|
+
/** The existing remote mobile surface, mounted only after device pairing succeeds. */
|
|
79
|
+
function PairedApp() {
|
|
53
80
|
const [route, setRoute] = useState({ kind: 'workspaces' });
|
|
54
81
|
const [initialWorkspaceId, setInitialWorkspaceId] = useState(() => mobileWorkspaceTarget(window.location.search));
|
|
55
82
|
const muxRef = useRef(undefined);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"App.test.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/App.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// @vitest-environment jsdom
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
|
5
|
+
import { RpcTransportError } from '../rpc.js';
|
|
6
|
+
const api = vi.hoisted(() => ({
|
|
7
|
+
fetchMobilePreferences: vi.fn(),
|
|
8
|
+
history: vi.fn(),
|
|
9
|
+
listSessions: vi.fn(),
|
|
10
|
+
listWorkspaces: vi.fn(),
|
|
11
|
+
prompt: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
vi.mock('../api.ts', () => api);
|
|
14
|
+
vi.mock('../mux.ts', () => ({
|
|
15
|
+
MuxClient: class {
|
|
16
|
+
start() { }
|
|
17
|
+
stop() { }
|
|
18
|
+
observe() { }
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
vi.mock('./WorkspaceView.tsx', () => ({ WorkspaceView: () => _jsx("div", { children: "workspace-ready" }) }));
|
|
22
|
+
vi.mock('./SessionListView.tsx', () => ({ SessionListView: () => _jsx("div", { children: "sessions-ready" }) }));
|
|
23
|
+
vi.mock('./ChatView.tsx', () => ({ ChatView: () => _jsx("div", { children: "chat-ready" }) }));
|
|
24
|
+
import { App, mobilePairStateForError } from './App.js';
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
cleanup();
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
});
|
|
29
|
+
describe('mobile paired-device gate', () => {
|
|
30
|
+
it('distinguishes a missing paired cookie from a transport outage', () => {
|
|
31
|
+
expect(mobilePairStateForError(new RpcTransportError('HTTP 403'))).toBe('unpaired');
|
|
32
|
+
expect(mobilePairStateForError(new RpcTransportError('HTTP 503'))).toBe('unavailable');
|
|
33
|
+
expect(mobilePairStateForError(new Error('offline'))).toBe('unavailable');
|
|
34
|
+
});
|
|
35
|
+
it('keeps a valid paired context usable after a failed QR token', async () => {
|
|
36
|
+
api.fetchMobilePreferences.mockResolvedValue({ mobileEnterToSend: true });
|
|
37
|
+
render(_jsx(App, { initialPairError: "\u914D\u5BF9\u94FE\u63A5\u5DF2\u88AB\u4F7F\u7528\u3002" }));
|
|
38
|
+
await waitFor(() => expect(screen.getByText('workspace-ready')).toBeDefined());
|
|
39
|
+
expect(screen.queryByRole('alert')).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
it('shows the pairing screen only when the mobile gateway rejects the device', async () => {
|
|
42
|
+
api.fetchMobilePreferences.mockRejectedValue(new RpcTransportError('HTTP 403'));
|
|
43
|
+
render(_jsx(App, { initialPairError: "\u914D\u5BF9\u94FE\u63A5\u5DF2\u88AB\u4F7F\u7528\u3002" }));
|
|
44
|
+
await waitFor(() => expect(screen.getByRole('alert').textContent).toContain('配对链接已被使用。'));
|
|
45
|
+
});
|
|
46
|
+
it('shows a retryable connection failure rather than mounting remote views on outage', async () => {
|
|
47
|
+
api.fetchMobilePreferences.mockRejectedValue(new RpcTransportError('HTTP 503'));
|
|
48
|
+
render(_jsx(App, {}));
|
|
49
|
+
await waitFor(() => expect(screen.getByRole('alert').textContent).toContain('无法连接到运行中的 DSH host。'));
|
|
50
|
+
expect(screen.queryByText('workspace-ready')).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -12,12 +12,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* permission pickers, both as bottom sheets.
|
|
13
13
|
*/
|
|
14
14
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
15
|
-
import { loadHistory, prompt } from
|
|
16
|
-
import { errorText, formatTime, staleHostHint } from
|
|
17
|
-
import { fetchMobilePreferences, models, selectModel, sendCommand } from
|
|
18
|
-
import { foldEvents } from
|
|
19
|
-
import { renderMarkdown } from
|
|
20
|
-
import { ThemeToggle } from
|
|
15
|
+
import { loadHistory, prompt } from './App.js';
|
|
16
|
+
import { errorText, formatTime, staleHostHint } from './App.js';
|
|
17
|
+
import { fetchMobilePreferences, models, selectModel, sendCommand } from '../api.js';
|
|
18
|
+
import { foldEvents } from '../messages.js';
|
|
19
|
+
import { renderMarkdown } from '../markdown.js';
|
|
20
|
+
import { ThemeToggle } from '../theme-toggle.js';
|
|
21
21
|
/**
|
|
22
22
|
* Hard cap on live events buffered while the initial history tail page is in
|
|
23
23
|
* flight. Beyond this the oldest buffered event is dropped and a follow-up
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
/** ChatView: collapsible message folds, toolbar chips, and the bottom sheets. */
|
|
4
4
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
6
|
-
import { ChatView, MAX_TAIL_BUFFER_EVENTS } from
|
|
6
|
+
import { ChatView, MAX_TAIL_BUFFER_EVENTS } from './ChatView.js';
|
|
7
7
|
// The api module is fully mocked; App.tsx's history wrapper is overridden to
|
|
8
8
|
// feed fixed history pages, its pure helpers (errorText / formatTime) stay real.
|
|
9
9
|
vi.mock('../api.ts', () => ({
|
|
@@ -20,8 +20,8 @@ vi.mock('./App.tsx', async (importOriginal) => {
|
|
|
20
20
|
prompt: vi.fn(async () => { }),
|
|
21
21
|
};
|
|
22
22
|
});
|
|
23
|
-
import { fetchMobilePreferences, models, selectModel, sendCommand } from
|
|
24
|
-
import { loadHistory, prompt } from
|
|
23
|
+
import { fetchMobilePreferences, models, selectModel, sendCommand } from '../api.js';
|
|
24
|
+
import { loadHistory, prompt } from './App.js';
|
|
25
25
|
const session = {
|
|
26
26
|
sessionId: 's-1',
|
|
27
27
|
title: '测试会话',
|
|
@@ -12,9 +12,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* the new chat — the same "new session opens" flow as the desktop UI.
|
|
13
13
|
*/
|
|
14
14
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
15
|
-
import { createSession, listSessions, listWorkspaces } from
|
|
16
|
-
import { errorText, formatTime, staleHostHint, toSessionView } from
|
|
17
|
-
import { ThemeToggle } from
|
|
15
|
+
import { createSession, listSessions, listWorkspaces } from '../api.js';
|
|
16
|
+
import { errorText, formatTime, staleHostHint, toSessionView } from './App.js';
|
|
17
|
+
import { ThemeToggle } from '../theme-toggle.js';
|
|
18
18
|
/** Rows that belong to the opened workspace (its owned session id set). */
|
|
19
19
|
function ownedItems(page, workspace) {
|
|
20
20
|
const owned = new Set(workspace.sessionIds);
|
|
@@ -3,14 +3,14 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
/** SessionListView: owned-row filtering, incremental pages, session creation. */
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
6
|
-
import { SessionListView } from
|
|
6
|
+
import { SessionListView } from './SessionListView.js';
|
|
7
7
|
// The api module is fully mocked; the view's App.tsx helpers stay real.
|
|
8
8
|
vi.mock('../api.ts', () => ({
|
|
9
9
|
listSessions: vi.fn(),
|
|
10
10
|
listWorkspaces: vi.fn(),
|
|
11
11
|
createSession: vi.fn(),
|
|
12
12
|
}));
|
|
13
|
-
import { createSession, listSessions, listWorkspaces } from
|
|
13
|
+
import { createSession, listSessions, listWorkspaces } from '../api.js';
|
|
14
14
|
const listSessionsMock = vi.mocked(listSessions);
|
|
15
15
|
const listWorkspacesMock = vi.mocked(listWorkspaces);
|
|
16
16
|
const createSessionMock = vi.mocked(createSession);
|
|
@@ -6,9 +6,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* until a workspace is opened).
|
|
7
7
|
*/
|
|
8
8
|
import { useEffect, useState } from 'react';
|
|
9
|
-
import { listWorkspaces } from
|
|
10
|
-
import { errorText } from
|
|
11
|
-
import { ThemeToggle } from
|
|
9
|
+
import { listWorkspaces } from '../api.js';
|
|
10
|
+
import { errorText } from './App.js';
|
|
11
|
+
import { ThemeToggle } from '../theme-toggle.js';
|
|
12
12
|
/**
|
|
13
13
|
* Render the workspace roster.
|
|
14
14
|
* @param props - the pick action.
|
|
@@ -3,12 +3,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
/** Mobile workspace landing: roster rendering and QR deep-link selection. */
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
|
6
|
-
import { mobileWorkspaceTarget } from
|
|
7
|
-
import { WorkspaceView } from
|
|
6
|
+
import { mobileWorkspaceTarget } from './App.js';
|
|
7
|
+
import { WorkspaceView } from './WorkspaceView.js';
|
|
8
8
|
vi.mock('../api.ts', () => ({
|
|
9
9
|
listWorkspaces: vi.fn(),
|
|
10
10
|
}));
|
|
11
|
-
import { listWorkspaces } from
|
|
11
|
+
import { listWorkspaces } from '../api.js';
|
|
12
12
|
const listWorkspacesMock = vi.mocked(listWorkspaces);
|
|
13
13
|
const workspaces = [
|
|
14
14
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAiDlD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,CAAA;IAClB,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,OAAO,CAAA;IAChC,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,8BAA8B;AAC9B,eAAO,MAAM,gBAAgB;aAC3B,MAAM,EAAE,mBAAmB;CACnB,CAAA;AAOV;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA4InE"}
|
package/lib/types/mobile-api.js
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* unary channel), gated identically.
|
|
20
20
|
*/
|
|
21
21
|
import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api/rpc';
|
|
22
|
-
import {
|
|
22
|
+
import { readBoundedJson, writeJson } from './http.js';
|
|
23
|
+
import { readCookie } from './gate.js';
|
|
23
24
|
/** Methods the phone surface may call. Everything else is refused. */
|
|
24
25
|
const MOBILE_ALLOWLIST = new Set([
|
|
25
26
|
'workspace.list',
|
|
@@ -95,10 +96,6 @@ export function makeMobileApiRoutes(deps) {
|
|
|
95
96
|
const gateOk = (req) => {
|
|
96
97
|
return touchDeviceFor(req);
|
|
97
98
|
};
|
|
98
|
-
const writeJson = (res, status, body) => {
|
|
99
|
-
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
|
|
100
|
-
res.end(JSON.stringify(body));
|
|
101
|
-
};
|
|
102
99
|
const handleMethod = async (req, res) => {
|
|
103
100
|
if (req.method !== 'POST') {
|
|
104
101
|
res.writeHead(405);
|
|
@@ -143,7 +140,12 @@ export function makeMobileApiRoutes(deps) {
|
|
|
143
140
|
return;
|
|
144
141
|
}
|
|
145
142
|
try {
|
|
146
|
-
|
|
143
|
+
// Cancel the host-side work when the phone goes away mid-call (the
|
|
144
|
+
// response stream closing before we answer means nobody is listening).
|
|
145
|
+
const abort = new AbortController();
|
|
146
|
+
res.on('close', () => { if (!res.writableEnded)
|
|
147
|
+
abort.abort(); });
|
|
148
|
+
const response = await dispatch(apiProxy, method, parsed?.payload, rpcId, abort.signal);
|
|
147
149
|
writeJson(res, 200, response);
|
|
148
150
|
}
|
|
149
151
|
catch (error) {
|
|
@@ -222,19 +224,10 @@ export function makeMobileApiRoutes(deps) {
|
|
|
222
224
|
}
|
|
223
225
|
/** Read a request body as JSON (bounded). */
|
|
224
226
|
async function readJsonBody(req) {
|
|
225
|
-
|
|
226
|
-
let size = 0;
|
|
227
|
-
for await (const chunk of req) {
|
|
228
|
-
const buffer = chunk;
|
|
229
|
-
size += buffer.length;
|
|
230
|
-
if (size > 64 * 1024)
|
|
231
|
-
throw new Error('body too large');
|
|
232
|
-
chunks.push(buffer);
|
|
233
|
-
}
|
|
234
|
-
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
227
|
+
return readBoundedJson(req, 64 * 1024);
|
|
235
228
|
}
|
|
236
229
|
/** Dispatch one allowlisted method through the host ApiProxy. */
|
|
237
|
-
async function dispatch(apiProxy, method, payload, rpcId) {
|
|
230
|
+
async function dispatch(apiProxy, method, payload, rpcId, signal) {
|
|
238
231
|
const request = { rpcId: RpcId(rpcId), payload };
|
|
239
232
|
if (method === 'session.list') {
|
|
240
233
|
const full = await apiProxy.sessions.list(request);
|
|
@@ -285,7 +278,7 @@ async function dispatch(apiProxy, method, payload, rpcId) {
|
|
|
285
278
|
if (method === 'session.history')
|
|
286
279
|
return wrap(await apiProxy.sessions.history(request));
|
|
287
280
|
if (method === 'session.search')
|
|
288
|
-
return wrap(await apiProxy.sessions.search(request, new AbortController().signal));
|
|
281
|
+
return wrap(await apiProxy.sessions.search(request, signal ?? new AbortController().signal));
|
|
289
282
|
if (method === 'session.prompt')
|
|
290
283
|
return wrap(await apiProxy.sessions.prompt(request));
|
|
291
284
|
if (method === 'session.models')
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** PWA registration for the standalone /m mobile surface. */
|
|
2
|
+
export declare const MOBILE_PWA_SCOPE = "/m/";
|
|
3
|
+
export declare const MOBILE_SERVICE_WORKER_URL = "/m/service-worker.js";
|
|
4
|
+
/** Register the mobile shell worker when the current browser supports it. */
|
|
5
|
+
export declare function registerMobilePwa(serviceWorker?: Pick<ServiceWorkerContainer, 'register'> | undefined): Promise<void>;
|
|
6
|
+
//# sourceMappingURL=mobile-pwa.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mobile-pwa.d.ts","sourceRoot":"","sources":["../../src/mobile-pwa.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,eAAO,MAAM,gBAAgB,QAAQ,CAAA;AACrC,eAAO,MAAM,yBAAyB,yBAAyB,CAAA;AAE/D,6EAA6E;AAC7E,wBAAsB,iBAAiB,CACrC,aAAa,GAAE,IAAI,CAAC,sBAAsB,EAAE,UAAU,CAAC,GAAG,SAAkF,GAC3I,OAAO,CAAC,IAAI,CAAC,CAWf"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** PWA registration for the standalone /m mobile surface. */
|
|
2
|
+
export const MOBILE_PWA_SCOPE = '/m/';
|
|
3
|
+
export const MOBILE_SERVICE_WORKER_URL = '/m/service-worker.js';
|
|
4
|
+
/** Register the mobile shell worker when the current browser supports it. */
|
|
5
|
+
export async function registerMobilePwa(serviceWorker = typeof navigator === 'undefined' ? undefined : navigator.serviceWorker) {
|
|
6
|
+
if (serviceWorker === undefined)
|
|
7
|
+
return;
|
|
8
|
+
try {
|
|
9
|
+
await serviceWorker.register(MOBILE_SERVICE_WORKER_URL, {
|
|
10
|
+
scope: MOBILE_PWA_SCOPE,
|
|
11
|
+
updateViaCache: 'none',
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// HTTP LAN origins cannot register workers; the online mobile UI still works.
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The mobile surface's page routes:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* already crosses the api/gate fence), so no host-side data plumbing is
|
|
7
|
-
* needed here — only static serving, loopback+paired-fence via the normal
|
|
8
|
-
* webserver route registration.
|
|
2
|
+
* The mobile surface's page routes: /m canonicalizes to /m/, which serves the
|
|
3
|
+
* standalone phone UI. The page and worker live in the /m/ scope so the PWA
|
|
4
|
+
* can cache only its static shell; the paired-device data channel remains at
|
|
5
|
+
* /m/api and is never handled by the worker.
|
|
9
6
|
*/
|
|
10
7
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
11
8
|
/**
|
|
12
9
|
* Build the mobile page routes.
|
|
13
|
-
* @returns the
|
|
10
|
+
* @returns Exact routes for the canonical page, static shell, and PWA assets.
|
|
14
11
|
*/
|
|
15
12
|
export declare function makeMobileRoutes(): WebRoute[];
|
|
16
13
|
//# sourceMappingURL=mobile-routes.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-routes.d.ts","sourceRoot":"","sources":["../../src/mobile-routes.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"mobile-routes.d.ts","sourceRoot":"","sources":["../../src/mobile-routes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAwH/D;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,QAAQ,EAAE,CAyE7C"}
|