@linxin666/dsh-remote-web-ui 0.1.20 → 0.2.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 +56 -26
- package/README.zh.md +20 -15
- 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 +272 -47
- package/lib/client.js.map +1 -1
- package/lib/index.js +316 -60
- package/lib/mobile.js +487 -56
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +28 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +147 -6
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +13 -2
- package/lib/types/client/RemoteSettingsCard.d.ts +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +1 -1
- package/lib/types/client/UpdateEntry.d.ts.map +1 -1
- package/lib/types/client/UpdateEntry.js +36 -3
- package/lib/types/client/UpdatePanel.d.ts.map +1 -1
- package/lib/types/client/UpdatePanel.js +2 -2
- package/lib/types/client/deep-link.js +1 -1
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +4 -0
- package/lib/types/gate.d.ts +9 -0
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +17 -4
- package/lib/types/index.d.ts +25 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +30 -13
- 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.d.ts +10 -0
- package/lib/types/mobile/api.d.ts.map +1 -1
- package/lib/types/mobile/api.js +4 -0
- package/lib/types/mobile/index.js +14 -1
- package/lib/types/mobile/messages.d.ts +24 -0
- package/lib/types/mobile/messages.d.ts.map +1 -1
- package/lib/types/mobile/messages.js +60 -23
- package/lib/types/mobile/messages.test.js +49 -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 +86 -0
- 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/views/App.d.ts +12 -5
- package/lib/types/mobile/views/App.d.ts.map +1 -1
- package/lib/types/mobile/views/App.js +34 -7
- 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.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +34 -11
- package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
- package/lib/types/mobile/views/SessionListView.js +30 -4
- package/lib/types/mobile/views/SessionListView.test.js +58 -1
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
- package/lib/types/mobile/views/WorkspaceView.js +4 -2
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +10 -1
- 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 +115 -30
- 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.map +1 -1
- package/lib/types/routes.js +23 -3
- package/lib/types/tunnel.d.ts +1 -0
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -2
- package/lib/types/update.d.ts +4 -4
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +35 -12
- package/package.json +12 -12
- package/src/client/PluginSettingsCard.tsx +229 -18
- package/src/client/RemoteEntry.tsx +12 -2
- package/src/client/RemoteSettingsCard.tsx +3 -2
- package/src/client/UpdateEntry.tsx +36 -4
- package/src/client/UpdatePanel.tsx +2 -0
- package/src/client/deep-link.ts +1 -1
- package/src/client/locales.ts +4 -0
- package/src/client/remote.module.css +26 -0
- package/src/client/settings-card.module.css +90 -1
- package/src/gate.ts +17 -3
- package/src/index.ts +46 -15
- package/src/mobile/PairRequiredView.test.tsx +28 -0
- package/src/mobile/PairRequiredView.tsx +58 -0
- package/src/mobile/api.ts +14 -1
- package/src/mobile/index.tsx +15 -1
- package/src/mobile/messages.test.ts +52 -1
- package/src/mobile/messages.ts +65 -22
- package/src/mobile/mobile-styles.ts +86 -0
- package/src/mobile/pairing.ts +77 -0
- package/src/mobile/views/App.test.tsx +62 -0
- package/src/mobile/views/App.tsx +53 -6
- package/src/mobile/views/ChatView.tsx +33 -10
- package/src/mobile/views/SessionListView.test.tsx +69 -1
- package/src/mobile/views/SessionListView.tsx +58 -5
- package/src/mobile/views/WorkspaceView.tsx +4 -2
- package/src/mobile-api.ts +9 -1
- package/src/mobile-pwa.ts +19 -0
- package/src/mobile-routes.ts +123 -30
- 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 +23 -3
- package/src/tunnel.ts +7 -2
- package/src/update.ts +42 -14
|
@@ -12,7 +12,7 @@ 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 '../api.js';
|
|
15
|
+
import { createSession, listAgentPresets, listSessions, listWorkspaces } from '../api.js';
|
|
16
16
|
import { errorText, formatTime, staleHostHint, toSessionView } from './App.js';
|
|
17
17
|
import { ThemeToggle } from '../theme-toggle.js';
|
|
18
18
|
/** Rows that belong to the opened workspace (its owned session id set). */
|
|
@@ -34,6 +34,9 @@ export function SessionListView({ workspace, onBack, onPick }) {
|
|
|
34
34
|
const [error, setError] = useState(undefined);
|
|
35
35
|
const [creating, setCreating] = useState(false);
|
|
36
36
|
const [createError, setCreateError] = useState(undefined);
|
|
37
|
+
const [presets, setPresets] = useState([]);
|
|
38
|
+
const [selectedPreset, setSelectedPreset] = useState(undefined);
|
|
39
|
+
const [presetsLoading, setPresetsLoading] = useState(true);
|
|
37
40
|
const cursorRef = useRef(undefined);
|
|
38
41
|
const busyRef = useRef(false);
|
|
39
42
|
// First page on mount. The workspace roster is re-read alongside so the
|
|
@@ -60,6 +63,25 @@ export function SessionListView({ workspace, onBack, onPick }) {
|
|
|
60
63
|
});
|
|
61
64
|
return () => { cancelled = true; };
|
|
62
65
|
}, [workspace]);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
let cancelled = false;
|
|
68
|
+
setPresets([]);
|
|
69
|
+
setSelectedPreset(undefined);
|
|
70
|
+
setPresetsLoading(true);
|
|
71
|
+
void listAgentPresets().then((roster) => {
|
|
72
|
+
if (cancelled)
|
|
73
|
+
return;
|
|
74
|
+
const usable = roster.presets.filter(preset => preset.broken === undefined);
|
|
75
|
+
setPresets(roster.presets);
|
|
76
|
+
setSelectedPreset((usable.find(preset => preset.isDefault) ?? usable[0])?.id);
|
|
77
|
+
setPresetsLoading(false);
|
|
78
|
+
}, () => {
|
|
79
|
+
if (cancelled)
|
|
80
|
+
return;
|
|
81
|
+
setPresetsLoading(false);
|
|
82
|
+
});
|
|
83
|
+
return () => { cancelled = true; };
|
|
84
|
+
}, [workspace]);
|
|
63
85
|
/** Pull the next page and append the rows that belong to this workspace. */
|
|
64
86
|
const loadMore = useCallback(() => {
|
|
65
87
|
if (busyRef.current)
|
|
@@ -87,7 +109,10 @@ export function SessionListView({ workspace, onBack, onPick }) {
|
|
|
87
109
|
return;
|
|
88
110
|
setCreating(true);
|
|
89
111
|
setCreateError(undefined);
|
|
90
|
-
void createSession({
|
|
112
|
+
void createSession({
|
|
113
|
+
workspaceId: workspace.workspaceId,
|
|
114
|
+
...(selectedPreset !== undefined ? { agentPreset: selectedPreset } : {}),
|
|
115
|
+
}).then((created) => {
|
|
91
116
|
setCreating(false);
|
|
92
117
|
const view = {
|
|
93
118
|
sessionId: created.sessionId,
|
|
@@ -102,7 +127,8 @@ export function SessionListView({ workspace, onBack, onPick }) {
|
|
|
102
127
|
setCreating(false);
|
|
103
128
|
setCreateError(errorText(reason));
|
|
104
129
|
});
|
|
105
|
-
}, [creating, workspace, onPick]);
|
|
130
|
+
}, [creating, workspace, onPick, selectedPreset]);
|
|
106
131
|
const createHint = createError !== undefined ? staleHostHint(createError) : undefined;
|
|
107
|
-
|
|
132
|
+
const selectedPresetEntry = presets.find(preset => preset.id === selectedPreset);
|
|
133
|
+
return (_jsxs("div", { className: "mobile", children: [_jsxs("header", { className: "mobile-header", children: [_jsx("button", { type: "button", className: "mobile-back", "aria-label": "\u8FD4\u56DE", onClick: onBack, children: "\u2039" }), _jsx("h1", { className: "mobile-title mobile-titleInline", children: workspace.title }), _jsx(ThemeToggle, {})] }), error !== undefined && _jsx("p", { className: "mobile-error mobile-pad", children: error }), _jsxs("div", { className: "mobile-create mobile-pad", children: [presets.length > 0 && (_jsxs("label", { className: "mobile-preset", children: [_jsx("span", { className: "mobile-presetLabel", children: "Agent \u6A21\u5F0F" }), _jsxs("select", { className: "mobile-presetSelect", value: selectedPreset ?? '', disabled: selectedPreset === undefined, onChange: (event) => { setSelectedPreset(event.target.value); }, children: [selectedPreset === undefined && _jsx("option", { value: "", children: "\u65E0\u53EF\u7528 Agent \u6A21\u5F0F" }), presets.map(preset => (_jsxs("option", { value: preset.id, disabled: preset.broken !== undefined, children: [preset.name ?? preset.id, preset.isDefault ? '(默认)' : '', preset.trust === 'user' ? '(本地)' : '', preset.broken !== undefined ? '(不可用)' : ''] }, preset.id)))] })] })), selectedPresetEntry?.description !== undefined && (_jsx("p", { className: "mobile-presetDescription", children: selectedPresetEntry.description })), _jsx("button", { type: "button", className: "mobile-new", disabled: creating || presetsLoading, onClick: () => { void handleCreate(); }, children: creating ? '创建中…' : '+ 新建会话' })] }), createError !== undefined && (_jsxs("p", { className: "mobile-error mobile-pad", children: [createError, createHint !== undefined && _jsx("span", { className: "mobile-hint", children: createHint })] })), _jsx("ul", { className: "mobile-list", children: rows.map(row => (_jsx("li", { children: _jsxs("button", { type: "button", className: "mobile-row", onClick: () => { onPick(row); }, children: [_jsxs("span", { className: "mobile-rowMain", children: [_jsxs("span", { className: "mobile-rowTitle", children: [row.blank ? '新会话' : row.title, row.running ? _jsx("span", { className: "mobile-live", children: "\u25CF" }) : null] }), _jsx("span", { className: "mobile-rowMeta", children: formatTime(row.updatedAt) })] }), _jsx("span", { className: "mobile-chevron", children: "\u203A" })] }) }, row.sessionId))) }), hasMore && (_jsx("div", { className: "mobile-pad", children: _jsx("button", { type: "button", className: "mobile-button mobile-block", disabled: loading, onClick: () => { void loadMore(); }, children: loading ? '加载中…' : '加载更多会话' }) })), !hasMore && rows.length === 0 && !loading && (_jsx("div", { className: "mobile-empty", children: _jsx("p", { className: "mobile-muted", children: "\u8BE5\u5DE5\u4F5C\u533A\u8FD8\u6CA1\u6709\u4F1A\u8BDD\uFF0C\u70B9\u4E0A\u65B9\u6309\u94AE\u65B0\u5EFA\u4E00\u4E2A" }) }))] }));
|
|
108
134
|
}
|
|
@@ -8,11 +8,13 @@ import { SessionListView } from './SessionListView.js';
|
|
|
8
8
|
vi.mock('../api.ts', () => ({
|
|
9
9
|
listSessions: vi.fn(),
|
|
10
10
|
listWorkspaces: vi.fn(),
|
|
11
|
+
listAgentPresets: vi.fn(),
|
|
11
12
|
createSession: vi.fn(),
|
|
12
13
|
}));
|
|
13
|
-
import { createSession, listSessions, listWorkspaces } from '../api.js';
|
|
14
|
+
import { createSession, listAgentPresets, listSessions, listWorkspaces } from '../api.js';
|
|
14
15
|
const listSessionsMock = vi.mocked(listSessions);
|
|
15
16
|
const listWorkspacesMock = vi.mocked(listWorkspaces);
|
|
17
|
+
const listAgentPresetsMock = vi.mocked(listAgentPresets);
|
|
16
18
|
const createSessionMock = vi.mocked(createSession);
|
|
17
19
|
const workspace = {
|
|
18
20
|
workspaceId: 'w-1',
|
|
@@ -34,6 +36,7 @@ function renderList(props = {}) {
|
|
|
34
36
|
beforeEach(() => {
|
|
35
37
|
listSessionsMock.mockResolvedValue({ items: [], hasMore: false });
|
|
36
38
|
listWorkspacesMock.mockResolvedValue([workspace]);
|
|
39
|
+
listAgentPresetsMock.mockResolvedValue({ presets: [], authorable: false, hasDocument: false });
|
|
37
40
|
createSessionMock.mockResolvedValue({ sessionId: 's-new' });
|
|
38
41
|
});
|
|
39
42
|
afterEach(() => {
|
|
@@ -87,6 +90,60 @@ describe('SessionListView creation', () => {
|
|
|
87
90
|
// The fresh row is prepended without waiting for a refetch.
|
|
88
91
|
expect(await screen.findByText('新会话')).toBeTruthy();
|
|
89
92
|
});
|
|
93
|
+
it('creates the session with the selected agent preset', async () => {
|
|
94
|
+
listAgentPresetsMock.mockResolvedValue({
|
|
95
|
+
presets: [
|
|
96
|
+
{ id: 'router-standard', name: 'Router Standard', trust: 'system', isDefault: true },
|
|
97
|
+
{ id: 'router-spec', name: 'Router Spec', description: '规格驱动模式', trust: 'system', isDefault: false },
|
|
98
|
+
],
|
|
99
|
+
authorable: false,
|
|
100
|
+
hasDocument: false,
|
|
101
|
+
});
|
|
102
|
+
renderList();
|
|
103
|
+
const picker = await screen.findByRole('combobox', { name: 'Agent 模式' });
|
|
104
|
+
expect(picker.value).toBe('router-standard');
|
|
105
|
+
fireEvent.change(picker, { target: { value: 'router-spec' } });
|
|
106
|
+
fireEvent.click(screen.getByRole('button', { name: '+ 新建会话' }));
|
|
107
|
+
await waitFor(() => {
|
|
108
|
+
expect(createSessionMock).toHaveBeenCalledWith({ workspaceId: 'w-1', agentPreset: 'router-spec' });
|
|
109
|
+
});
|
|
110
|
+
expect(await screen.findByText('规格驱动模式')).toBeTruthy();
|
|
111
|
+
});
|
|
112
|
+
it('skips a broken default preset and disables it in the picker', async () => {
|
|
113
|
+
listAgentPresetsMock.mockResolvedValue({
|
|
114
|
+
presets: [
|
|
115
|
+
{ id: 'broken-default', trust: 'system', isDefault: true, broken: 'missing plugin' },
|
|
116
|
+
{ id: 'local-router', name: 'Local Router', trust: 'user', isDefault: false },
|
|
117
|
+
],
|
|
118
|
+
authorable: false,
|
|
119
|
+
hasDocument: false,
|
|
120
|
+
});
|
|
121
|
+
renderList();
|
|
122
|
+
const picker = await screen.findByRole('combobox', { name: 'Agent 模式' });
|
|
123
|
+
expect(picker.value).toBe('local-router');
|
|
124
|
+
expect(screen.getByRole('option', { name: 'broken-default(默认)(不可用)' }).hasAttribute('disabled')).toBe(true);
|
|
125
|
+
expect(screen.getByRole('option', { name: 'Local Router(本地)' })).toBeTruthy();
|
|
126
|
+
});
|
|
127
|
+
it('keeps default session creation available when the preset roster fails', async () => {
|
|
128
|
+
listAgentPresetsMock.mockRejectedValue(new Error('preset roster unavailable'));
|
|
129
|
+
renderList();
|
|
130
|
+
await screen.findByText(/还没有会话/);
|
|
131
|
+
const button = await screen.findByRole('button', { name: '+ 新建会话' });
|
|
132
|
+
await waitFor(() => { expect(button.hasAttribute('disabled')).toBe(false); });
|
|
133
|
+
fireEvent.click(button);
|
|
134
|
+
await waitFor(() => {
|
|
135
|
+
expect(createSessionMock).toHaveBeenCalledWith({ workspaceId: 'w-1' });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
it('waits for the preset roster before enabling session creation', async () => {
|
|
139
|
+
let resolvePresets = () => { };
|
|
140
|
+
listAgentPresetsMock.mockReturnValue(new Promise(resolve => { resolvePresets = resolve; }));
|
|
141
|
+
renderList();
|
|
142
|
+
const button = screen.getByRole('button', { name: '+ 新建会话' });
|
|
143
|
+
expect(button.hasAttribute('disabled')).toBe(true);
|
|
144
|
+
resolvePresets({ presets: [], authorable: false, hasDocument: false });
|
|
145
|
+
await waitFor(() => { expect(button.hasAttribute('disabled')).toBe(false); });
|
|
146
|
+
});
|
|
90
147
|
it('keeps the button disabled while a creation is in flight', async () => {
|
|
91
148
|
let resolveCreate = () => { };
|
|
92
149
|
createSessionMock.mockReturnValue(new Promise(resolve => { resolveCreate = resolve; }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkspaceView.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/WorkspaceView.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,IAAI,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAKjG,sCAAsC;AACtC,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,yCAAyC;IACzC,MAAM,CAAC,SAAS,EAAE,YAAY,GAAG,IAAI,CAAA;CACtC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAAE,kBAAkB,EAAE,MAAM,EAAE,EAAE,kBAAkB,+
|
|
1
|
+
{"version":3,"file":"WorkspaceView.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/WorkspaceView.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,IAAI,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAKjG,sCAAsC;AACtC,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,yCAAyC;IACzC,MAAM,CAAC,SAAS,EAAE,YAAY,GAAG,IAAI,CAAA;CACtC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAAE,kBAAkB,EAAE,MAAM,EAAE,EAAE,kBAAkB,+BA4F/E"}
|
|
@@ -17,6 +17,8 @@ import { ThemeToggle } from '../theme-toggle.js';
|
|
|
17
17
|
export function WorkspaceView({ initialWorkspaceId, onPick }) {
|
|
18
18
|
const [items, setItems] = useState(undefined);
|
|
19
19
|
const [error, setError] = useState(undefined);
|
|
20
|
+
// Bumped by the retry button to re-run the roster fetch effect.
|
|
21
|
+
const [reload, setReload] = useState(0);
|
|
20
22
|
useEffect(() => {
|
|
21
23
|
let cancelled = false;
|
|
22
24
|
void listWorkspaces().then((rows) => {
|
|
@@ -36,9 +38,9 @@ export function WorkspaceView({ initialWorkspaceId, onPick }) {
|
|
|
36
38
|
setError(errorText(reason));
|
|
37
39
|
});
|
|
38
40
|
return () => { cancelled = true; };
|
|
39
|
-
}, [initialWorkspaceId, onPick]);
|
|
41
|
+
}, [initialWorkspaceId, onPick, reload]);
|
|
40
42
|
if (error !== undefined) {
|
|
41
|
-
return (_jsxs("div", { className: "mobile", children: [_jsxs("header", { className: "mobile-header", children: [_jsx("h1", { className: "mobile-title", children: "\u5DE5\u4F5C\u533A" }), _jsx(ThemeToggle, {})] }), _jsxs("div", { className: "mobile-empty", children: [_jsxs("p", { className: "mobile-error", children: ["\u52A0\u8F7D\u5931\u8D25\uFF1A", error] }), _jsx("button", { type: "button", className: "mobile-button", onClick: () => { setError(undefined); setItems(undefined); }, children: "\u91CD\u8BD5" })] })] }));
|
|
43
|
+
return (_jsxs("div", { className: "mobile", children: [_jsxs("header", { className: "mobile-header", children: [_jsx("h1", { className: "mobile-title", children: "\u5DE5\u4F5C\u533A" }), _jsx(ThemeToggle, {})] }), _jsxs("div", { className: "mobile-empty", children: [_jsxs("p", { className: "mobile-error", children: ["\u52A0\u8F7D\u5931\u8D25\uFF1A", error] }), _jsx("button", { type: "button", className: "mobile-button", onClick: () => { setError(undefined); setItems(undefined); setReload(n => n + 1); }, children: "\u91CD\u8BD5" })] })] }));
|
|
42
44
|
}
|
|
43
45
|
if (items === undefined) {
|
|
44
46
|
return (_jsxs("div", { className: "mobile", children: [_jsxs("header", { className: "mobile-header", children: [_jsx("h1", { className: "mobile-title", children: "\u5DE5\u4F5C\u533A" }), _jsx(ThemeToggle, {})] }), _jsx("div", { className: "mobile-empty", children: _jsx("p", { className: "mobile-muted", children: "\u52A0\u8F7D\u4E2D\u2026" }) })] }));
|
|
@@ -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;AAwDlD,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
|
@@ -21,9 +21,16 @@
|
|
|
21
21
|
import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api/rpc';
|
|
22
22
|
import { readBoundedJson, writeJson } from './http.js';
|
|
23
23
|
import { readCookie } from './gate.js';
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Methods the phone surface may call. Everything else is refused HERE — but
|
|
26
|
+
* note the paired-device cookie also passes the global api/gate for the full
|
|
27
|
+
* ApiProxy surface (gate.ts), so a paired phone is a full-control credential:
|
|
28
|
+
* the allowlist only constrains this /m/api proxy, not the cookie's reach.
|
|
29
|
+
* stop() revokes every device; there is no per-device revocation yet.
|
|
30
|
+
*/
|
|
25
31
|
const MOBILE_ALLOWLIST = new Set([
|
|
26
32
|
'workspace.list',
|
|
33
|
+
'agentPreset.list',
|
|
27
34
|
'session.create',
|
|
28
35
|
'session.list',
|
|
29
36
|
'session.history',
|
|
@@ -273,6 +280,8 @@ async function dispatch(apiProxy, method, payload, rpcId, signal) {
|
|
|
273
280
|
});
|
|
274
281
|
if (method === 'workspace.list')
|
|
275
282
|
return wrap(await apiProxy.workspace.list(request));
|
|
283
|
+
if (method === 'agentPreset.list')
|
|
284
|
+
return wrap(await apiProxy.agentPresets.list(request));
|
|
276
285
|
if (method === 'session.create')
|
|
277
286
|
return wrap(await apiProxy.sessions.create(request));
|
|
278
287
|
if (method === 'session.history')
|
|
@@ -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"}
|
|
@@ -1,24 +1,26 @@
|
|
|
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 { readFile } from 'node:fs/promises';
|
|
11
8
|
import { existsSync } from 'node:fs';
|
|
12
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
+
const MOBILE_ROOT = '/m/';
|
|
11
|
+
const MOBILE_BUNDLE_URL = '/m/mobile.js';
|
|
12
|
+
const MOBILE_MANIFEST_URL = '/m/manifest.webmanifest';
|
|
13
|
+
const MOBILE_WORKER_URL = '/m/service-worker.js';
|
|
14
|
+
const MOBILE_OFFLINE_URL = '/m/offline.html';
|
|
13
15
|
/** The standalone mobile bundle (built artifact, next to this file's own lib output). */
|
|
14
16
|
function mobileBundlePath() {
|
|
15
17
|
return fileURLToPath(new URL('../lib/mobile.js', import.meta.url));
|
|
16
18
|
}
|
|
17
|
-
/**
|
|
18
|
-
function
|
|
19
|
-
return fileURLToPath(new URL('../assets/
|
|
19
|
+
/** A package asset available to the host after a registry or git install. */
|
|
20
|
+
function mobileAssetPath(name) {
|
|
21
|
+
return fileURLToPath(new URL('../assets/' + name, import.meta.url));
|
|
20
22
|
}
|
|
21
|
-
/** The mobile page shell:
|
|
23
|
+
/** The mobile page shell: self-contained, with no document-level user data. */
|
|
22
24
|
function pageHtml(bundleUrl) {
|
|
23
25
|
return [
|
|
24
26
|
'<!doctype html>',
|
|
@@ -28,29 +30,73 @@ function pageHtml(bundleUrl) {
|
|
|
28
30
|
'<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">',
|
|
29
31
|
'<meta name="theme-color" content="#f3f5f9">',
|
|
30
32
|
'<meta name="referrer" content="no-referrer">',
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
'<meta name="mobile-web-app-capable" content="yes">',
|
|
34
|
+
'<meta name="apple-mobile-web-app-capable" content="yes">',
|
|
35
|
+
'<meta name="apple-mobile-web-app-title" content="DSH Remote">',
|
|
36
|
+
'<link rel="manifest" href="/m/manifest.webmanifest">',
|
|
34
37
|
'<link rel="apple-touch-icon" href="/m/apple-touch-icon.png">',
|
|
35
38
|
'<title>移动端远程控制</title>',
|
|
36
39
|
'</head>',
|
|
37
40
|
'<body>',
|
|
38
41
|
'<div id="root"></div>',
|
|
39
|
-
|
|
42
|
+
'<script type="module" src="' + bundleUrl + '"></script>',
|
|
40
43
|
'</body>',
|
|
41
44
|
'</html>',
|
|
42
45
|
].join('');
|
|
43
46
|
}
|
|
44
|
-
/**
|
|
47
|
+
/** The installable app identity and icon declarations for the /m scope. */
|
|
48
|
+
function manifestJson() {
|
|
49
|
+
return JSON.stringify({
|
|
50
|
+
id: MOBILE_ROOT,
|
|
51
|
+
name: 'DSH Remote',
|
|
52
|
+
short_name: 'DSH Remote',
|
|
53
|
+
start_url: MOBILE_ROOT,
|
|
54
|
+
scope: MOBILE_ROOT,
|
|
55
|
+
display: 'standalone',
|
|
56
|
+
background_color: '#151424',
|
|
57
|
+
theme_color: '#f3f5f9',
|
|
58
|
+
icons: [
|
|
59
|
+
{ src: '/m/icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
|
|
60
|
+
{ src: '/m/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' },
|
|
61
|
+
],
|
|
62
|
+
}, null, 2);
|
|
63
|
+
}
|
|
64
|
+
/** A static fallback that contains no paired-device, session, or workspace data. */
|
|
65
|
+
function offlineHtml() {
|
|
66
|
+
return [
|
|
67
|
+
'<!doctype html>',
|
|
68
|
+
'<html lang="zh-CN">',
|
|
69
|
+
'<head>',
|
|
70
|
+
'<meta charset="utf-8">',
|
|
71
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">',
|
|
72
|
+
'<meta name="theme-color" content="#151424">',
|
|
73
|
+
'<title>DSH Remote</title>',
|
|
74
|
+
'<style>body{margin:0;background:#151424;color:#f3f5f9;font:16px system-ui,sans-serif}main{box-sizing:border-box;display:grid;min-height:100vh;place-content:center;padding:32px;text-align:center}h1{margin:0 0 12px;font-size:24px}p{margin:0;color:#c4c7d8;line-height:1.5}</style>',
|
|
75
|
+
'</head>',
|
|
76
|
+
'<body><main><h1>DSH Remote</h1><p>Cannot reach the running DSH host. Restore the connection and reopen the app.</p></main></body>',
|
|
77
|
+
'</html>',
|
|
78
|
+
].join('');
|
|
79
|
+
}
|
|
80
|
+
/** Send a small static UTF-8 body with revalidation headers. */
|
|
45
81
|
function writeStatic(res, status, type, body) {
|
|
46
82
|
res.writeHead(status, {
|
|
47
|
-
'content-type':
|
|
83
|
+
'content-type': type + '; charset=utf-8',
|
|
84
|
+
'cache-control': 'no-cache',
|
|
85
|
+
'referrer-policy': 'no-referrer',
|
|
86
|
+
});
|
|
87
|
+
res.end(body);
|
|
88
|
+
}
|
|
89
|
+
/** Send the worker with the same scope it registers for, without HTTP caching. */
|
|
90
|
+
function writeServiceWorker(res, body) {
|
|
91
|
+
res.writeHead(200, {
|
|
92
|
+
'content-type': 'text/javascript; charset=utf-8',
|
|
48
93
|
'cache-control': 'no-cache',
|
|
49
94
|
'referrer-policy': 'no-referrer',
|
|
95
|
+
'service-worker-allowed': MOBILE_ROOT,
|
|
50
96
|
});
|
|
51
97
|
res.end(body);
|
|
52
98
|
}
|
|
53
|
-
/** Send
|
|
99
|
+
/** Send a PNG body without decoding it to UTF-8. */
|
|
54
100
|
function writePng(res, status, body) {
|
|
55
101
|
res.writeHead(status, {
|
|
56
102
|
'content-type': 'image/png',
|
|
@@ -59,16 +105,32 @@ function writePng(res, status, body) {
|
|
|
59
105
|
});
|
|
60
106
|
res.end(body);
|
|
61
107
|
}
|
|
108
|
+
/** Canonicalize the old /m route while retaining pair and workspace query parameters. */
|
|
109
|
+
function redirectToMobileRoot(req, res) {
|
|
110
|
+
const url = new URL(req.url ?? '/m', 'http://x');
|
|
111
|
+
res.writeHead(308, {
|
|
112
|
+
location: MOBILE_ROOT + url.search,
|
|
113
|
+
'cache-control': 'no-store',
|
|
114
|
+
'referrer-policy': 'no-referrer',
|
|
115
|
+
});
|
|
116
|
+
res.end();
|
|
117
|
+
}
|
|
62
118
|
/**
|
|
63
119
|
* Build the mobile page routes.
|
|
64
|
-
* @returns the
|
|
120
|
+
* @returns Exact routes for the canonical page, static shell, and PWA assets.
|
|
65
121
|
*/
|
|
66
122
|
export function makeMobileRoutes() {
|
|
67
123
|
const handlePage = (_req, res) => {
|
|
68
|
-
writeStatic(res, 200, 'text/html', pageHtml(
|
|
124
|
+
writeStatic(res, 200, 'text/html', pageHtml(MOBILE_BUNDLE_URL));
|
|
125
|
+
};
|
|
126
|
+
const handleManifest = (_req, res) => {
|
|
127
|
+
writeStatic(res, 200, 'application/manifest+json', manifestJson());
|
|
128
|
+
};
|
|
129
|
+
const handleOffline = (_req, res) => {
|
|
130
|
+
writeStatic(res, 200, 'text/html', offlineHtml());
|
|
69
131
|
};
|
|
70
132
|
// The bundle is immutable for the process lifetime (a rebuild requires a
|
|
71
|
-
// host restart), so the
|
|
133
|
+
// host restart), so the body is read from disk once, not per phone.
|
|
72
134
|
let bundleBody;
|
|
73
135
|
const handleBundle = async (_req, res) => {
|
|
74
136
|
if (bundleBody === undefined) {
|
|
@@ -87,23 +149,46 @@ export function makeMobileRoutes() {
|
|
|
87
149
|
}
|
|
88
150
|
writeStatic(res, 200, 'text/javascript', bundleBody);
|
|
89
151
|
};
|
|
90
|
-
|
|
91
|
-
|
|
152
|
+
let workerBody;
|
|
153
|
+
const handleWorker = async (_req, res) => {
|
|
154
|
+
if (workerBody === undefined) {
|
|
155
|
+
const path = mobileAssetPath('mobile-service-worker.js');
|
|
156
|
+
if (!existsSync(path)) {
|
|
157
|
+
writeStatic(res, 503, 'text/plain', 'mobile service worker not found');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
workerBody = await readFile(path, 'utf8');
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
writeStatic(res, 500, 'text/plain', 'failed to read the mobile service worker');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
writeServiceWorker(res, workerBody);
|
|
169
|
+
};
|
|
170
|
+
const makeIconHandler = (asset) => async (_req, res) => {
|
|
171
|
+
const path = mobileAssetPath(asset);
|
|
92
172
|
if (!existsSync(path)) {
|
|
93
|
-
writeStatic(res, 404, 'text/plain', '
|
|
173
|
+
writeStatic(res, 404, 'text/plain', asset + ' not found');
|
|
94
174
|
return;
|
|
95
175
|
}
|
|
96
176
|
try {
|
|
97
|
-
|
|
98
|
-
writePng(res, 200, body);
|
|
177
|
+
writePng(res, 200, await readFile(path));
|
|
99
178
|
}
|
|
100
179
|
catch {
|
|
101
|
-
writeStatic(res, 500, 'text/plain', 'failed to read
|
|
180
|
+
writeStatic(res, 500, 'text/plain', 'failed to read ' + asset);
|
|
102
181
|
}
|
|
103
182
|
};
|
|
104
183
|
return [
|
|
105
|
-
{ kind: 'exact', path: '/m', handler:
|
|
106
|
-
{ kind: 'exact', path:
|
|
107
|
-
{ kind: 'exact', path:
|
|
184
|
+
{ kind: 'exact', path: '/m', handler: redirectToMobileRoot },
|
|
185
|
+
{ kind: 'exact', path: MOBILE_ROOT, handler: handlePage },
|
|
186
|
+
{ kind: 'exact', path: MOBILE_BUNDLE_URL, handler: handleBundle },
|
|
187
|
+
{ kind: 'exact', path: MOBILE_MANIFEST_URL, handler: handleManifest },
|
|
188
|
+
{ kind: 'exact', path: MOBILE_WORKER_URL, handler: handleWorker },
|
|
189
|
+
{ kind: 'exact', path: MOBILE_OFFLINE_URL, handler: handleOffline },
|
|
190
|
+
{ kind: 'exact', path: '/m/apple-touch-icon.png', handler: makeIconHandler('apple-touch-icon.png') },
|
|
191
|
+
{ kind: 'exact', path: '/m/icon-192.png', handler: makeIconHandler('mobile-icon-192.png') },
|
|
192
|
+
{ kind: 'exact', path: '/m/icon-512.png', handler: makeIconHandler('mobile-icon-512.png') },
|
|
108
193
|
];
|
|
109
194
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional cordis service other host plugins can ctx.get without taking a
|
|
3
|
+
* package dependency: a live paired-device cookie is an allow path on
|
|
4
|
+
* routes that sit outside /api (and therefore outside api/gate).
|
|
5
|
+
*/
|
|
6
|
+
import type { IncomingMessage } from 'node:http';
|
|
7
|
+
import { Service, type Context } from '@deepseek-ai/cordis';
|
|
8
|
+
/** Named lookup key sibling plugins pass to ctx.get. */
|
|
9
|
+
export declare const REMOTE_WEB_UI_PAIRING = "remoteWebUiPairing";
|
|
10
|
+
/**
|
|
11
|
+
* Pairing identity for one HTTP request. Structural: consumers must not
|
|
12
|
+
* import this class, only the method shape.
|
|
13
|
+
*/
|
|
14
|
+
export declare class RemoteWebUiPairing extends Service {
|
|
15
|
+
private readonly check;
|
|
16
|
+
/**
|
|
17
|
+
* @param ctx - host plugin context.
|
|
18
|
+
* @param check - live cookie + session predicate (re-read per request).
|
|
19
|
+
*/
|
|
20
|
+
constructor(ctx: Context, check: (request: IncomingMessage) => boolean);
|
|
21
|
+
/**
|
|
22
|
+
* Whether the request carries a live paired-device cookie.
|
|
23
|
+
* @param request - the incoming HTTP request.
|
|
24
|
+
* @returns true when the session is live and was refreshed.
|
|
25
|
+
*/
|
|
26
|
+
isPairedDevice(request: IncomingMessage): boolean;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=pairing-access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing-access.d.ts","sourceRoot":"","sources":["../../src/pairing-access.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,EAAE,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAE3D,wDAAwD;AACxD,eAAO,MAAM,qBAAqB,uBAAuB,CAAA;AAEzD;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,OAAO;IAO3C,OAAO,CAAC,QAAQ,CAAC,KAAK;IANxB;;;OAGG;IACH,YACE,GAAG,EAAE,OAAO,EACK,KAAK,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,EAG9D;IAED;;;;OAIG;IACH,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAEhD;CACF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
2
|
+
/** Named lookup key sibling plugins pass to ctx.get. */
|
|
3
|
+
export const REMOTE_WEB_UI_PAIRING = 'remoteWebUiPairing';
|
|
4
|
+
/**
|
|
5
|
+
* Pairing identity for one HTTP request. Structural: consumers must not
|
|
6
|
+
* import this class, only the method shape.
|
|
7
|
+
*/
|
|
8
|
+
export class RemoteWebUiPairing extends Service {
|
|
9
|
+
check;
|
|
10
|
+
/**
|
|
11
|
+
* @param ctx - host plugin context.
|
|
12
|
+
* @param check - live cookie + session predicate (re-read per request).
|
|
13
|
+
*/
|
|
14
|
+
constructor(ctx, check) {
|
|
15
|
+
super(ctx, REMOTE_WEB_UI_PAIRING);
|
|
16
|
+
this.check = check;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether the request carries a live paired-device cookie.
|
|
20
|
+
* @param request - the incoming HTTP request.
|
|
21
|
+
* @returns true when the session is live and was refreshed.
|
|
22
|
+
*/
|
|
23
|
+
isPairedDevice(request) {
|
|
24
|
+
return this.check(request);
|
|
25
|
+
}
|
|
26
|
+
}
|
package/lib/types/pairing.d.ts
CHANGED
|
@@ -87,6 +87,13 @@ export interface PairingConfig {
|
|
|
87
87
|
maxDevices: number;
|
|
88
88
|
/** Cookie name carrying the device id. */
|
|
89
89
|
cookieName: string;
|
|
90
|
+
/**
|
|
91
|
+
* Path to a JSON file where paired device sessions are persisted. When
|
|
92
|
+
* set, sessions survive process restarts (the phone keeps its 365-day
|
|
93
|
+
* cookie), so re-pairing after a dsh web restart is not required. When
|
|
94
|
+
* unset (default), sessions stay memory-only — the previous behavior.
|
|
95
|
+
*/
|
|
96
|
+
devicesFile?: string;
|
|
90
97
|
}
|
|
91
98
|
/** Result of one accept() attempt. */
|
|
92
99
|
export type AcceptResult = {
|
|
@@ -138,6 +145,26 @@ export declare class PairingService {
|
|
|
138
145
|
* @param clock - clock/entropy source (injectable for tests).
|
|
139
146
|
*/
|
|
140
147
|
constructor(config: PairingConfig, clock?: PairingClock);
|
|
148
|
+
/**
|
|
149
|
+
* Restore device sessions persisted by a previous process run. A corrupt
|
|
150
|
+
* or missing file is tolerated (an empty device table, never a throw) —
|
|
151
|
+
* persistence is an availability convenience, not a security boundary.
|
|
152
|
+
*/
|
|
153
|
+
private loadPersisted;
|
|
154
|
+
/** FIFO-cap the device table (a persisted file may outlive a lowered cap). */
|
|
155
|
+
private clampToMaxDevices;
|
|
156
|
+
/**
|
|
157
|
+
* Write the current device table to the configured file. Called on the
|
|
158
|
+
* mutation boundaries that change the set of live sessions (accept and
|
|
159
|
+
* stop); `lastSeenAt` refreshes are deliberately not persisted here so a
|
|
160
|
+
* per-request write storm is avoided — after a restart the phone's first
|
|
161
|
+
* heartbeat re-warms its own session.
|
|
162
|
+
*
|
|
163
|
+
* Device ids are session credentials (the gate authorizes requests by the
|
|
164
|
+
* cookie's device id), so the file is written 0600 via a temp file and
|
|
165
|
+
* atomic rename; a crash mid-write can never leave a half-written store.
|
|
166
|
+
*/
|
|
167
|
+
private persist;
|
|
141
168
|
/** The default LAN base URL (the first interface; undefined when not LAN-reachable). */
|
|
142
169
|
get lanBaseUrl(): string | undefined;
|
|
143
170
|
/** The LAN base URL for one specific literal (undefined when not constructible). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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"}
|