@linxin666/dsh-remote-web-ui 0.2.0 → 0.2.2
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 +17 -8
- package/README.zh.md +5 -5
- package/lib/client.js +259 -43
- package/lib/client.js.map +1 -1
- package/lib/index.js +26 -7
- package/lib/mobile.js +209 -39
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +22 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +142 -3
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +11 -0
- package/lib/types/client/RemoteSettingsCard.d.ts +2 -2
- package/lib/types/client/UpdateEntry.d.ts.map +1 -1
- package/lib/types/client/UpdateEntry.js +8 -5
- package/lib/types/client/UpdatePanel.d.ts +3 -1
- package/lib/types/client/UpdatePanel.d.ts.map +1 -1
- package/lib/types/client/UpdatePanel.js +3 -3
- 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 +8 -4
- package/lib/types/client/pair-api.d.ts +1 -6
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +1 -10
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +2 -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/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 +38 -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/routes.d.ts.map +1 -1
- package/lib/types/routes.js +22 -2
- 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 +1 -1
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +1 -1
- package/package.json +1 -2
- package/src/client/PluginSettingsCard.tsx +219 -18
- package/src/client/RemoteEntry.tsx +10 -0
- package/src/client/RemoteSettingsCard.tsx +2 -2
- package/src/client/UpdateEntry.tsx +9 -5
- package/src/client/UpdatePanel.tsx +12 -1
- package/src/client/locales.ts +8 -4
- package/src/client/pair-api.ts +1 -10
- package/src/client/settings-card.module.css +90 -1
- package/src/gate.ts +2 -0
- package/src/mobile/api.ts +14 -1
- package/src/mobile/messages.test.ts +52 -1
- package/src/mobile/messages.ts +65 -22
- package/src/mobile/mobile-styles.ts +38 -0
- package/src/mobile/mobile-theme.test.ts +1 -1
- 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/routes.ts +22 -2
- package/src/tunnel.ts +7 -2
- package/src/update.ts +2 -2
|
@@ -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')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,CAAC,EAAgB,MAAM,KAAK,CAAA;AACrC,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAIhG;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA6BtG;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOxE;AAKD,8CAA8C;AAC9C,eAAO,MAAM,UAAU;aACrB,KAAK,EAAE,iBAAiB;aACxB,MAAM,EAAE,kBAAkB;aAC1B,IAAI,EAAE,gBAAgB;aACtB,SAAS,EAAE,qBAAqB;aAChC,MAAM,EAAE,kBAAkB;aAC1B,MAAM,EAAE,kBAAkB;CAClB,CAAA;AAEV;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;iBAG7B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,uBAAuB,gCAA6B,CAAA;AA6BjE,gDAAgD;AAChD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD;;OAEG;IACH,YAAY,OAAO,EAAE,cAAc,EAElC;IAED,4DAA4D;IAC5D,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAepD;IAED,kEAAkE;IAClE,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAUpC;IAED,wCAAwC;IACxC,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,OAAO,EAAE,cAAc,CAAA;IACvB,0EAA0E;IAC1E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;CAChC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAAE,CAAC,EAAgB,MAAM,KAAK,CAAA;AACrC,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAIhG;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CA6BtG;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOxE;AAKD,8CAA8C;AAC9C,eAAO,MAAM,UAAU;aACrB,KAAK,EAAE,iBAAiB;aACxB,MAAM,EAAE,kBAAkB;aAC1B,IAAI,EAAE,gBAAgB;aACtB,SAAS,EAAE,qBAAqB;aAChC,MAAM,EAAE,kBAAkB;aAC1B,MAAM,EAAE,kBAAkB;CAClB,CAAA;AAEV;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;iBAG7B,CAAA;AACF,eAAO,MAAM,mBAAmB;;iBAE9B,CAAA;AACF,eAAO,MAAM,uBAAuB,gCAA6B,CAAA;AA6BjE,gDAAgD;AAChD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD;;OAEG;IACH,YAAY,OAAO,EAAE,cAAc,EAElC;IAED,4DAA4D;IAC5D,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAepD;IAED,kEAAkE;IAClE,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAUpC;IAED,wCAAwC;IACxC,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,OAAO,EAAE,cAAc,CAAA;IACvB,0EAA0E;IAC1E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;CAChC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,QAAQ,EAAE,CAwM3D"}
|
package/lib/types/routes.js
CHANGED
|
@@ -200,7 +200,16 @@ export function makeRoutes(deps) {
|
|
|
200
200
|
const ACCEPT_MAX_ATTEMPTS = 10;
|
|
201
201
|
const ACCEPT_WINDOW_MS = 30_000;
|
|
202
202
|
const rateLimitAccept = (req) => {
|
|
203
|
-
const
|
|
203
|
+
const socketIp = req.socket?.remoteAddress ?? 'unknown';
|
|
204
|
+
// Behind the auto-tunnel every internet client arrives from 127.0.0.1,
|
|
205
|
+
// so a single shared bucket would let one attacker keep the legitimate
|
|
206
|
+
// owner rate-limited. Partition the availability bucket by the first
|
|
207
|
+
// client-visible XFF hop (set by the tunnel edge): XFF is untrusted for
|
|
208
|
+
// authentication and only separates buckets, it never grants access.
|
|
209
|
+
const forwarded = typeof req.headers['x-forwarded-for'] === 'string'
|
|
210
|
+
? (req.headers['x-forwarded-for'].split(',')[0] ?? '').trim()
|
|
211
|
+
: undefined;
|
|
212
|
+
const ip = forwarded === undefined || forwarded === '' ? socketIp : socketIp + '|' + forwarded;
|
|
204
213
|
const nowMs = Date.now();
|
|
205
214
|
// The map lives as long as the plugin: prune expired windows once the
|
|
206
215
|
// table grows past a modest size so distinct source IPs (LAN clients,
|
|
@@ -287,6 +296,9 @@ export function makeRoutes(deps) {
|
|
|
287
296
|
writeJson(res, result.code === 'used' ? 409 : 404, { ok: false, code: result.code });
|
|
288
297
|
return;
|
|
289
298
|
}
|
|
299
|
+
// No Secure attribute: LAN pairing runs over plain HTTP (the cookie must
|
|
300
|
+
// work there), and the same cookie rides HTTPS on the tunnel. Lax keeps
|
|
301
|
+
// top-level navigations working while blocking cross-site subrequests.
|
|
290
302
|
res.writeHead(200, {
|
|
291
303
|
'content-type': 'application/json; charset=utf-8',
|
|
292
304
|
'set-cookie': [
|
|
@@ -337,7 +349,15 @@ export function makeRoutes(deps) {
|
|
|
337
349
|
return;
|
|
338
350
|
}
|
|
339
351
|
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
340
|
-
|
|
352
|
+
const paired = deviceId !== undefined && service.hasDevice(deviceId);
|
|
353
|
+
const snapshot = service.snapshot();
|
|
354
|
+
// Unpaired LAN/tunnel clients get only the pairing-relevant fields; the
|
|
355
|
+
// token expiry, device roster, and public tunnel URL are an oracle for
|
|
356
|
+
// targeting and timing and stay behind a live device cookie.
|
|
357
|
+
const visible = paired
|
|
358
|
+
? snapshot
|
|
359
|
+
: { phase: snapshot.phase, lanAvailable: snapshot.lanAvailable, lanAddresses: snapshot.lanAddresses };
|
|
360
|
+
writeJson(res, 200, { ok: true, paired, ...visible });
|
|
341
361
|
};
|
|
342
362
|
const handleEvents = (req, res) => {
|
|
343
363
|
if (!requireMethod(req, res, 'GET'))
|
package/lib/types/tunnel.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,0EAA0E;AAC1E,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;AAEvE,+BAA+B;AAC/B,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAA;IAClB,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;IAC9D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;IAC/D,IAAI,IAAI,OAAO,CAAA;CAChB;AAED,kFAAkF;AAClF,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,YAAY,CAAA;IAC7C,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wCAAwC;IACxC,KAAK,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAA;CAC5F;AAkBD;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqF;IAE3G,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,GAAG,CAAoB;IAC/B,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAI;
|
|
1
|
+
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"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"}
|
package/lib/types/tunnel.js
CHANGED
|
@@ -46,6 +46,9 @@ export class TunnelManager {
|
|
|
46
46
|
urlTimer;
|
|
47
47
|
restartTimer;
|
|
48
48
|
attempts = 0;
|
|
49
|
+
// Generation counter: a stale ensureBinary resolution from an earlier
|
|
50
|
+
// start() must not spawn a second handle after a stop/start cycle.
|
|
51
|
+
generation = 0;
|
|
49
52
|
stopping = false;
|
|
50
53
|
urlListeners = new Set();
|
|
51
54
|
phaseListeners = new Set();
|
|
@@ -81,6 +84,7 @@ export class TunnelManager {
|
|
|
81
84
|
this.stopping = false;
|
|
82
85
|
this.targetUrl = targetUrl;
|
|
83
86
|
this.attempts = 0;
|
|
87
|
+
this.generation += 1;
|
|
84
88
|
this.attempt();
|
|
85
89
|
}
|
|
86
90
|
/** Stop the tunnel for good: no restarts, no state. */
|
|
@@ -107,12 +111,13 @@ export class TunnelManager {
|
|
|
107
111
|
attempt() {
|
|
108
112
|
if (this.stopping || this.targetUrl === undefined)
|
|
109
113
|
return;
|
|
114
|
+
const gen = this.generation;
|
|
110
115
|
this.setPhase('starting');
|
|
111
116
|
this.handle = undefined;
|
|
112
117
|
this.url = undefined;
|
|
113
118
|
this.error = undefined;
|
|
114
119
|
void this.ensureBinary().then(() => {
|
|
115
|
-
if (this.stopping || this.targetUrl === undefined)
|
|
120
|
+
if (this.stopping || this.targetUrl === undefined || gen !== this.generation)
|
|
116
121
|
return;
|
|
117
122
|
const handle = this.factory(this.targetUrl);
|
|
118
123
|
this.handle = handle;
|
|
@@ -139,7 +144,7 @@ export class TunnelManager {
|
|
|
139
144
|
});
|
|
140
145
|
}).catch((value) => {
|
|
141
146
|
// Binary install failed (no network, no platform build): report it.
|
|
142
|
-
if (this.stopping || this.targetUrl === undefined)
|
|
147
|
+
if (this.stopping || this.targetUrl === undefined || gen !== this.generation)
|
|
143
148
|
return;
|
|
144
149
|
const message = value instanceof Error ? value.message : String(value);
|
|
145
150
|
this.fail(`could not obtain the cloudflared binary: ${message}`);
|
package/lib/types/update.d.ts
CHANGED
|
@@ -137,7 +137,7 @@ export declare function familyChildren(anchorManifest: Record<string, unknown>):
|
|
|
137
137
|
* @param timeoutMs - probe timeout.
|
|
138
138
|
* @returns the latest version string, or undefined on any failure.
|
|
139
139
|
*/
|
|
140
|
-
export declare function fetchLatestVersion(name: string, fetchImpl: (url: string) => Promise<{
|
|
140
|
+
export declare function fetchLatestVersion(name: string, fetchImpl: (url: string, init?: RequestInit) => Promise<{
|
|
141
141
|
ok: boolean;
|
|
142
142
|
json(): Promise<unknown>;
|
|
143
143
|
}>, timeoutMs?: number): Promise<string | undefined>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAE1C,iDAAiD;AACjD,eAAO,MAAM,YAAY,+BAA+B,CAAA;AAExD,oEAAoE;AACpE,eAAO,MAAM,YAAY,gBAAgB,CAAA;AAEzC,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,8BAA8B,CAAA;AAE5D,+EAA+E;AAC/E,eAAO,MAAM,YAAY,iCAAiC,CAAA;AAQ1D,0EAA0E;AAC1E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,2DAA2D;IAC3D,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CASlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CA2B5D;AAYD,yDAAyD;AACzD,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,kBAAkB,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAahF;AAED,6DAA6D;AAC7D,KAAK,cAAc,GAAG,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAA;AAE9D,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAG1D;AAED,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,iGAAiG;IACjG,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,SAAS,CAAA;IAChC,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qDAAqD;IACrD,QAAQ,EAAE,mBAAmB,EAAE,CAAA;IAC/B,qDAAqD;IACrD,QAAQ,EAAE,OAAO,CAAA;IACjB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,4EAA4E;IAC5E,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IAC9C,oEAAoE;IACpE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CACvD;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACjD,MAAM,GAAG,SAAS,CAUpB;AAED,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC3B,2BAA2B;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAA;IAClB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE;IAAE,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAE,GACpC,YAAY,GAAG;IAAE,KAAK,EAAE,WAAW,GAAG,MAAM,CAAA;CAAE,CAoBhD;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAQhF;AAiBD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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"}
|
package/lib/types/update.js
CHANGED
|
@@ -218,7 +218,7 @@ export async function fetchLatestVersion(name, fetchImpl, timeoutMs = 10_000) {
|
|
|
218
218
|
const controller = new AbortController();
|
|
219
219
|
const timer = setTimeout(() => { controller.abort(); }, timeoutMs);
|
|
220
220
|
try {
|
|
221
|
-
const response = await fetchImpl(NPM_REGISTRY + '/' + name.replace('/', '%2F') + '/latest');
|
|
221
|
+
const response = await fetchImpl(NPM_REGISTRY + '/' + name.replace('/', '%2F') + '/latest', { signal: controller.signal });
|
|
222
222
|
if (!response.ok)
|
|
223
223
|
return undefined;
|
|
224
224
|
const body = await response.json();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
3
|
"description": "Mobile remote control for the dsh web GUI: scan-to-pair QR entry beside the settings button, one-time pairing tokens, live device status, and revocable mobile sessions",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -78,7 +78,6 @@
|
|
|
78
78
|
"@types/react": "~18.3.1",
|
|
79
79
|
"@types/react-dom": "~18.3.0",
|
|
80
80
|
"jsdom": "29.1.1",
|
|
81
|
-
"lightningcss": "^1.32.0",
|
|
82
81
|
"react": "^18.2.0",
|
|
83
82
|
"react-dom": "^18.2.0",
|
|
84
83
|
"tsdown": "^0.22.2",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* ui-plugin-config PluginCard in a self-contained slice.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { useState, type ReactNode } from 'react'
|
|
11
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState, type KeyboardEvent, type ReactNode } from 'react'
|
|
12
12
|
import type { CardShell } from './settings-form.ts'
|
|
13
13
|
import css from './settings-card.module.css'
|
|
14
14
|
|
|
@@ -255,6 +255,210 @@ export function ValueField(props: FieldProps & {
|
|
|
255
255
|
)
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
const NON_SKIN_BODY_MARKERS = new Set(['dshSkinCenter', 'dshSidebarCollapsed'])
|
|
259
|
+
// 检测使用使用皮肤,用了皮肤退回原生select样式,防止样式冲突。默认外观下使用优化后的select样式。
|
|
260
|
+
function isSkinActive(): boolean {
|
|
261
|
+
const datasetList = Object.keys(document.body.dataset)
|
|
262
|
+
const isActive = datasetList.some(key => key.startsWith('dsh') && !NON_SKIN_BODY_MARKERS.has(key))
|
|
263
|
+
return isActive
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface SelectOption {
|
|
267
|
+
value: string
|
|
268
|
+
label: string
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const SELECT_CLOSE_MS = 100
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The shared dual-mode select control. While an appearance skin is active it
|
|
275
|
+
* renders the legacy native `<select>` untouched, so element-level skin
|
|
276
|
+
* selectors keep working; under the default appearance it renders a
|
|
277
|
+
* self-drawn `role="listbox"` popup whose open/close is transition-animated.
|
|
278
|
+
* Staged cards reach it through BooleanField/ChoiceField; immediate-apply
|
|
279
|
+
* editors (the side-card prefs) bind it directly through onEdit.
|
|
280
|
+
* 双模式下拉框:皮肤激活时用原生 select,默认外观用自绘动画弹层。
|
|
281
|
+
*/
|
|
282
|
+
export function SelectField(props: {
|
|
283
|
+
id: string
|
|
284
|
+
options: ReadonlyArray<SelectOption>
|
|
285
|
+
value: string
|
|
286
|
+
disabled: boolean
|
|
287
|
+
invalid: boolean
|
|
288
|
+
onEdit: (text: string) => void
|
|
289
|
+
}) {
|
|
290
|
+
const { id, options, value } = props
|
|
291
|
+
const [open, setOpen] = useState(false)
|
|
292
|
+
const [closing, setClosing] = useState(false)
|
|
293
|
+
const [phase, setPhase] = useState<'initial' | 'open'>('initial')
|
|
294
|
+
const [activeIndex, setActiveIndex] = useState(0)
|
|
295
|
+
const closeTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
296
|
+
const wrapRef = useRef<HTMLDivElement | null>(null)
|
|
297
|
+
const popupRef = useRef<HTMLDivElement | null>(null)
|
|
298
|
+
|
|
299
|
+
const currentIndex = () => {
|
|
300
|
+
const index = options.findIndex(option => option.value === value)
|
|
301
|
+
return index >= 0 ? index : 0
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const close = useCallback(() => {
|
|
305
|
+
if (closeTimer.current !== undefined) clearTimeout(closeTimer.current)
|
|
306
|
+
setClosing(true)
|
|
307
|
+
closeTimer.current = setTimeout(() => {
|
|
308
|
+
setClosing(false)
|
|
309
|
+
setOpen(false)
|
|
310
|
+
}, SELECT_CLOSE_MS)
|
|
311
|
+
}, [])
|
|
312
|
+
|
|
313
|
+
const openPopup = () => {
|
|
314
|
+
if (closeTimer.current !== undefined) clearTimeout(closeTimer.current)
|
|
315
|
+
setActiveIndex(currentIndex())
|
|
316
|
+
setPhase('initial')
|
|
317
|
+
setClosing(false)
|
|
318
|
+
setOpen(true)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const commit = (index: number) => {
|
|
322
|
+
const option = options[index]
|
|
323
|
+
if (option) props.onEdit(option.value)
|
|
324
|
+
close()
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const onTriggerClick = () => {
|
|
328
|
+
if (props.disabled) return
|
|
329
|
+
if (open && !closing) close()
|
|
330
|
+
else openPopup()
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
|
|
334
|
+
if (props.disabled) return
|
|
335
|
+
const count = options.length
|
|
336
|
+
switch (event.key) {
|
|
337
|
+
case 'ArrowDown':
|
|
338
|
+
case 'ArrowUp':
|
|
339
|
+
case 'Enter':
|
|
340
|
+
case ' ':
|
|
341
|
+
event.preventDefault()
|
|
342
|
+
if (!open) {
|
|
343
|
+
openPopup()
|
|
344
|
+
} else if (!closing) {
|
|
345
|
+
if (event.key === 'ArrowDown') setActiveIndex(index => (index + 1) % count)
|
|
346
|
+
else if (event.key === 'ArrowUp') setActiveIndex(index => (index - 1 + count) % count)
|
|
347
|
+
else commit(activeIndex)
|
|
348
|
+
}
|
|
349
|
+
break
|
|
350
|
+
case 'Escape':
|
|
351
|
+
if (open) {
|
|
352
|
+
event.preventDefault()
|
|
353
|
+
event.stopPropagation()
|
|
354
|
+
close()
|
|
355
|
+
}
|
|
356
|
+
break
|
|
357
|
+
case 'Tab':
|
|
358
|
+
if (open) close()
|
|
359
|
+
break
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
useEffect(() => () => {
|
|
364
|
+
if (closeTimer.current !== undefined) clearTimeout(closeTimer.current)
|
|
365
|
+
}, [])
|
|
366
|
+
useLayoutEffect(() => {
|
|
367
|
+
if (open && !closing && phase === 'initial') {
|
|
368
|
+
void popupRef.current?.offsetHeight
|
|
369
|
+
setPhase('open')
|
|
370
|
+
}
|
|
371
|
+
}, [open, closing, phase])
|
|
372
|
+
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
if (!open) return
|
|
375
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
376
|
+
const target = event.target
|
|
377
|
+
if (target instanceof Node && !wrapRef.current?.contains(target)) close()
|
|
378
|
+
}
|
|
379
|
+
document.addEventListener('pointerdown', onPointerDown)
|
|
380
|
+
return () => document.removeEventListener('pointerdown', onPointerDown)
|
|
381
|
+
}, [open, close])
|
|
382
|
+
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
if (props.disabled && open) close()
|
|
385
|
+
}, [props.disabled, open, close])
|
|
386
|
+
|
|
387
|
+
// 使用了皮肤则退回原生select样式防止样式冲突。
|
|
388
|
+
if (isSkinActive()) {
|
|
389
|
+
return (
|
|
390
|
+
<select
|
|
391
|
+
id={id}
|
|
392
|
+
className={css.select}
|
|
393
|
+
value={value}
|
|
394
|
+
disabled={props.disabled}
|
|
395
|
+
onChange={(event) => { props.onEdit(event.target.value) }}
|
|
396
|
+
>
|
|
397
|
+
{options.map(option => (
|
|
398
|
+
<option key={option.value} value={option.value}>{option.label}</option>
|
|
399
|
+
))}
|
|
400
|
+
</select>
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const label = options.find(option => option.value === value)?.label ?? ''
|
|
405
|
+
const popupClass = closing
|
|
406
|
+
? `${css.selectPopup} ${css.selectPopupClose}`
|
|
407
|
+
: phase === 'open'
|
|
408
|
+
? `${css.selectPopup} ${css.selectPopupOpen}`
|
|
409
|
+
: css.selectPopup
|
|
410
|
+
return (
|
|
411
|
+
<div className={css.selectWrap} ref={wrapRef}>
|
|
412
|
+
<button
|
|
413
|
+
type="button"
|
|
414
|
+
id={id}
|
|
415
|
+
className={`${css.select} ${css.selectButton}`}
|
|
416
|
+
disabled={props.disabled}
|
|
417
|
+
aria-haspopup="listbox"
|
|
418
|
+
aria-expanded={open}
|
|
419
|
+
aria-activedescendant={open ? `${id}-o${activeIndex}` : undefined}
|
|
420
|
+
aria-invalid={props.invalid || undefined}
|
|
421
|
+
onClick={onTriggerClick}
|
|
422
|
+
onKeyDown={onKeyDown}
|
|
423
|
+
>
|
|
424
|
+
<span className={css.selectLabel}>{label}</span>
|
|
425
|
+
<svg
|
|
426
|
+
width="14"
|
|
427
|
+
height="14"
|
|
428
|
+
viewBox="0 0 14 14"
|
|
429
|
+
fill="none"
|
|
430
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
431
|
+
className={open ? `${css.selectChevron} ${css.selectChevronOpen}` : css.selectChevron}
|
|
432
|
+
aria-hidden="true"
|
|
433
|
+
>
|
|
434
|
+
<path
|
|
435
|
+
d="M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z"
|
|
436
|
+
fill="currentColor"
|
|
437
|
+
/>
|
|
438
|
+
</svg>
|
|
439
|
+
</button>
|
|
440
|
+
{open
|
|
441
|
+
? (
|
|
442
|
+
<div className={popupClass} role="listbox" ref={popupRef}>
|
|
443
|
+
{options.map((option, index) => (
|
|
444
|
+
<div
|
|
445
|
+
key={option.value}
|
|
446
|
+
id={`${id}-o${index}`}
|
|
447
|
+
role="option"
|
|
448
|
+
aria-selected={option.value === value}
|
|
449
|
+
className={`${css.selectOption}${option.value === value ? ` ${css.selectOptionSelected}` : ''}${index === activeIndex && !closing ? ` ${css.selectOptionActive}` : ''}`}
|
|
450
|
+
onClick={() => { commit(index) }}
|
|
451
|
+
>
|
|
452
|
+
{option.label}
|
|
453
|
+
</div>
|
|
454
|
+
))}
|
|
455
|
+
</div>
|
|
456
|
+
)
|
|
457
|
+
: null}
|
|
458
|
+
</div>
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
|
|
258
462
|
/** A staged boolean field: 继承 / 开 / 关. */
|
|
259
463
|
export function BooleanField(props: FieldProps & {
|
|
260
464
|
/** Copy for the inherit option. */
|
|
@@ -284,17 +488,18 @@ export function BooleanField(props: FieldProps & {
|
|
|
284
488
|
)
|
|
285
489
|
: null}
|
|
286
490
|
</div>
|
|
287
|
-
<
|
|
491
|
+
<SelectField
|
|
288
492
|
id={props.id}
|
|
289
|
-
|
|
493
|
+
options={[
|
|
494
|
+
{ value: '', label: props.inheritLabel },
|
|
495
|
+
{ value: 'true', label: props.onLabel },
|
|
496
|
+
{ value: 'false', label: props.offLabel },
|
|
497
|
+
]}
|
|
290
498
|
value={props.text}
|
|
291
499
|
disabled={props.disabled}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
<option value="true">{props.onLabel}</option>
|
|
296
|
-
<option value="false">{props.offLabel}</option>
|
|
297
|
-
</select>
|
|
500
|
+
invalid={props.invalid}
|
|
501
|
+
onEdit={props.onEdit}
|
|
502
|
+
/>
|
|
298
503
|
<p className={css.hint}>{props.hint}</p>
|
|
299
504
|
</div>
|
|
300
505
|
)
|
|
@@ -327,18 +532,14 @@ export function ChoiceField(props: FieldProps & {
|
|
|
327
532
|
)
|
|
328
533
|
: null}
|
|
329
534
|
</div>
|
|
330
|
-
<
|
|
535
|
+
<SelectField
|
|
331
536
|
id={props.id}
|
|
332
|
-
|
|
537
|
+
options={[{ value: '', label: props.inheritLabel }, ...props.choices]}
|
|
333
538
|
value={props.text}
|
|
334
539
|
disabled={props.disabled}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
{props.choices.map(choice => (
|
|
339
|
-
<option key={choice.value} value={choice.value}>{choice.label}</option>
|
|
340
|
-
))}
|
|
341
|
-
</select>
|
|
540
|
+
invalid={props.invalid}
|
|
541
|
+
onEdit={props.onEdit}
|
|
542
|
+
/>
|
|
342
543
|
<p className={props.invalid ? css.invalid : css.hint}>
|
|
343
544
|
{props.invalid ? props.invalidLabel : props.hint}
|
|
344
545
|
</p>
|
|
@@ -57,6 +57,10 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
|
57
57
|
useEffect(() => { stateRef.current = state }, [state])
|
|
58
58
|
const [copied, setCopied] = useState(false)
|
|
59
59
|
const eventSource = useRef<EventSource | undefined>(undefined)
|
|
60
|
+
// Generation counter for the open flow: closing (or re-opening) the panel
|
|
61
|
+
// bumps it, so an in-flight issue() that resolves after a close does not
|
|
62
|
+
// spawn a stray EventSource.
|
|
63
|
+
const openSeq = useRef(0)
|
|
60
64
|
|
|
61
65
|
// The current workspace (the recent-workspace projection the shell's New
|
|
62
66
|
// Session flow targets) — the deep-link target for the phone.
|
|
@@ -104,8 +108,13 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
|
104
108
|
}, [workspaceId])
|
|
105
109
|
|
|
106
110
|
const openPanel = useCallback(async (): Promise<void> => {
|
|
111
|
+
const seq = ++openSeq.current
|
|
107
112
|
setOpen(true)
|
|
108
113
|
const next = await mint()
|
|
114
|
+
// A close (or re-open) during the await invalidates this issue: skip the
|
|
115
|
+
// state write and the stream so a panel closed mid-mint neither leaks an
|
|
116
|
+
// EventSource nor resurrects a stale QR.
|
|
117
|
+
if (seq !== openSeq.current) return
|
|
109
118
|
setState(next)
|
|
110
119
|
// Live status: the desktop panel mirrors the pairing service state. The
|
|
111
120
|
// stream makes sense in the ready state and on the lan-required banner —
|
|
@@ -143,6 +152,7 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
|
143
152
|
}, [mint])
|
|
144
153
|
|
|
145
154
|
const closePanel = useCallback(() => {
|
|
155
|
+
openSeq.current += 1
|
|
146
156
|
closeEventSource()
|
|
147
157
|
setOpen(false)
|
|
148
158
|
}, [closeEventSource])
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The remote-control settings card: pairing security and device limits.
|
|
3
|
-
* Registers into the `
|
|
4
|
-
*
|
|
3
|
+
* Registers into the `web-ui.plugin.item` child slot the Web UI plugin group
|
|
4
|
+
* renders, bound to the `remote-web-ui` settings namespace.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|