@linxin666/dsh-remote-web-ui 0.2.9 → 0.3.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 -8
- package/README.zh.md +7 -2
- package/lib/client.js +65 -34
- package/lib/client.js.map +1 -1
- package/lib/index.js +645 -61
- package/lib/mobile.js +874 -64
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +35 -5
- package/lib/types/client/remote-channel.d.ts +4 -5
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +13 -28
- package/lib/types/http.d.ts +37 -8
- package/lib/types/http.d.ts.map +1 -1
- package/lib/types/http.js +76 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +25 -1
- package/lib/types/mobile/api.d.ts +57 -0
- package/lib/types/mobile/api.d.ts.map +1 -1
- package/lib/types/mobile/api.js +29 -0
- package/lib/types/mobile/markdown.d.ts +7 -1
- package/lib/types/mobile/markdown.d.ts.map +1 -1
- package/lib/types/mobile/markdown.js +16 -7
- package/lib/types/mobile/markdown.test.js +8 -0
- 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 +325 -6
- package/lib/types/mobile/views/ChatView.d.ts +10 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +205 -8
- package/lib/types/mobile/views/ChatView.stream.test.d.ts +2 -0
- package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +1 -0
- package/lib/types/mobile/views/ChatView.stream.test.js +205 -0
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
- package/lib/types/mobile/views/WorkspaceView.js +46 -5
- package/lib/types/mobile/views/WorkspaceView.test.js +129 -2
- package/lib/types/mobile-api.d.ts +9 -3
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +61 -16
- package/lib/types/mobile-pending.d.ts +38 -0
- package/lib/types/mobile-pending.d.ts.map +1 -0
- package/lib/types/mobile-pending.js +59 -0
- package/lib/types/paired-model-catalog.d.ts +17 -0
- package/lib/types/paired-model-catalog.d.ts.map +1 -0
- package/lib/types/paired-model-catalog.js +252 -0
- package/lib/types/pairing.d.ts +13 -4
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +13 -5
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.d.ts +26 -0
- package/lib/types/remote-channel-boot.d.ts.map +1 -0
- package/lib/types/remote-channel-boot.js +128 -0
- package/lib/types/remote-channel-rules.d.ts +40 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -0
- package/lib/types/remote-channel-rules.js +32 -0
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +12 -23
- package/lib/types/uuid-polyfill.d.ts +10 -0
- package/lib/types/uuid-polyfill.d.ts.map +1 -0
- package/lib/types/uuid-polyfill.js +27 -0
- package/package.json +1 -1
- package/src/client/index.ts +41 -5
- package/src/client/remote-channel.ts +18 -28
- package/src/http.ts +87 -12
- package/src/index.ts +27 -2
- package/src/mobile/api.ts +91 -0
- package/src/mobile/markdown.test.ts +9 -0
- package/src/mobile/markdown.ts +17 -7
- package/src/mobile/mobile-styles.ts +326 -6
- package/src/mobile/views/ChatView.stream.test.tsx +231 -0
- package/src/mobile/views/ChatView.tsx +328 -10
- package/src/mobile/views/WorkspaceView.test.tsx +163 -2
- package/src/mobile/views/WorkspaceView.tsx +152 -26
- package/src/mobile-api.ts +59 -17
- package/src/mobile-pending.ts +88 -0
- package/src/paired-model-catalog.ts +224 -0
- package/src/pairing.ts +13 -5
- package/src/remote-api.ts +0 -1
- package/src/remote-channel-boot.ts +131 -0
- package/src/remote-channel-rules.ts +62 -0
- package/src/routes.ts +13 -24
- package/src/uuid-polyfill.ts +28 -0
|
@@ -2,14 +2,18 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
// @vitest-environment jsdom
|
|
3
3
|
/** Mobile workspace landing: roster rendering and QR deep-link selection. */
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
|
5
|
+
import { cleanup, render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
6
6
|
import { mobileWorkspaceTarget } from "./App.js";
|
|
7
7
|
import { WorkspaceView } from "./WorkspaceView.js";
|
|
8
8
|
vi.mock('../api.ts', () => ({
|
|
9
9
|
listWorkspaces: vi.fn(),
|
|
10
|
+
listDirectory: vi.fn(),
|
|
11
|
+
createWorkspace: vi.fn(),
|
|
10
12
|
}));
|
|
11
|
-
import { listWorkspaces } from "../api.js";
|
|
13
|
+
import { listWorkspaces, listDirectory, createWorkspace } from "../api.js";
|
|
12
14
|
const listWorkspacesMock = vi.mocked(listWorkspaces);
|
|
15
|
+
const listDirectoryMock = vi.mocked(listDirectory);
|
|
16
|
+
const createWorkspaceMock = vi.mocked(createWorkspace);
|
|
13
17
|
const workspaces = [
|
|
14
18
|
{
|
|
15
19
|
workspaceId: 'ws-1',
|
|
@@ -54,3 +58,126 @@ describe('mobile workspace deep link', () => {
|
|
|
54
58
|
expect(onPick).not.toHaveBeenCalled();
|
|
55
59
|
});
|
|
56
60
|
});
|
|
61
|
+
describe('mobile workspace creation', () => {
|
|
62
|
+
it('shows new workspace button and navigates to directory browser', async () => {
|
|
63
|
+
listWorkspacesMock.mockResolvedValue(workspaces);
|
|
64
|
+
listDirectoryMock.mockResolvedValue({
|
|
65
|
+
path: '/home/user',
|
|
66
|
+
home: '/home/user',
|
|
67
|
+
crumbs: [{ name: '', path: '/', hidden: false }, { name: 'home', path: '/home', hidden: false }, { name: 'user', path: '/home/user', hidden: false }],
|
|
68
|
+
entries: [
|
|
69
|
+
{ name: 'projects', path: '/home/user/projects', hidden: false },
|
|
70
|
+
{ name: '.config', path: '/home/user/.config', hidden: true }
|
|
71
|
+
],
|
|
72
|
+
truncated: false
|
|
73
|
+
});
|
|
74
|
+
const onPick = vi.fn();
|
|
75
|
+
render(_jsx(WorkspaceView, { onPick: onPick }));
|
|
76
|
+
// Wait for workspaces to load
|
|
77
|
+
const createBtn = await screen.findByText('+ 新建工作区');
|
|
78
|
+
// Click create button
|
|
79
|
+
fireEvent.click(createBtn);
|
|
80
|
+
// Verify directory browser renders
|
|
81
|
+
expect(await screen.findByText('选择目录')).toBeTruthy();
|
|
82
|
+
// Verify crumbs
|
|
83
|
+
expect(await screen.findByText('user')).toBeTruthy();
|
|
84
|
+
// Verify entries
|
|
85
|
+
expect(await screen.findByText('📁 projects')).toBeTruthy();
|
|
86
|
+
expect(await screen.findByText('📁 .config')).toBeTruthy();
|
|
87
|
+
});
|
|
88
|
+
it('navigates into directory and allows breadcrumb navigation', async () => {
|
|
89
|
+
listWorkspacesMock.mockResolvedValue(workspaces);
|
|
90
|
+
// Initial dir
|
|
91
|
+
listDirectoryMock.mockResolvedValueOnce({
|
|
92
|
+
path: '/home/user',
|
|
93
|
+
home: '/home/user',
|
|
94
|
+
crumbs: [{ name: '', path: '/', hidden: false }, { name: 'home', path: '/home', hidden: false }, { name: 'user', path: '/home/user', hidden: false }],
|
|
95
|
+
entries: [{ name: 'projects', path: '/home/user/projects', hidden: false }],
|
|
96
|
+
truncated: false
|
|
97
|
+
});
|
|
98
|
+
const onPick = vi.fn();
|
|
99
|
+
render(_jsx(WorkspaceView, { onPick: onPick }));
|
|
100
|
+
// Open dir browser
|
|
101
|
+
fireEvent.click(await screen.findByText('+ 新建工作区'));
|
|
102
|
+
// Wait for first dir listing
|
|
103
|
+
const projBtn = await screen.findByText('📁 projects');
|
|
104
|
+
// Prepare next listing
|
|
105
|
+
listDirectoryMock.mockResolvedValueOnce({
|
|
106
|
+
path: '/home/user/projects',
|
|
107
|
+
home: '/home/user',
|
|
108
|
+
crumbs: [
|
|
109
|
+
{ name: '', path: '/', hidden: false },
|
|
110
|
+
{ name: 'home', path: '/home', hidden: false },
|
|
111
|
+
{ name: 'user', path: '/home/user', hidden: false },
|
|
112
|
+
{ name: 'projects', path: '/home/user/projects', hidden: false }
|
|
113
|
+
],
|
|
114
|
+
entries: [{ name: 'foo', path: '/home/user/projects/foo', hidden: false }],
|
|
115
|
+
truncated: false
|
|
116
|
+
});
|
|
117
|
+
// Click into folder
|
|
118
|
+
fireEvent.click(projBtn);
|
|
119
|
+
// Verify new contents load
|
|
120
|
+
expect(await screen.findByText('📁 foo')).toBeTruthy();
|
|
121
|
+
// Check breadcrumb
|
|
122
|
+
const homeCrumb = screen.getByText('home');
|
|
123
|
+
expect(homeCrumb).toBeTruthy();
|
|
124
|
+
// Click breadcrumb
|
|
125
|
+
listDirectoryMock.mockResolvedValueOnce({
|
|
126
|
+
path: '/home',
|
|
127
|
+
home: '/home/user',
|
|
128
|
+
crumbs: [{ name: '', path: '/', hidden: false }, { name: 'home', path: '/home', hidden: false }],
|
|
129
|
+
entries: [{ name: 'user', path: '/home/user', hidden: false }],
|
|
130
|
+
truncated: false
|
|
131
|
+
});
|
|
132
|
+
fireEvent.click(homeCrumb);
|
|
133
|
+
expect(await screen.findByText('📁 user')).toBeTruthy();
|
|
134
|
+
});
|
|
135
|
+
it('creates workspace successfully', async () => {
|
|
136
|
+
listWorkspacesMock.mockResolvedValue(workspaces);
|
|
137
|
+
listDirectoryMock.mockResolvedValue({
|
|
138
|
+
path: '/home/user/projects/foo',
|
|
139
|
+
home: '/home/user',
|
|
140
|
+
crumbs: [{ name: '', path: '/', hidden: false }, { name: 'foo', path: '/home/user/projects/foo', hidden: false }],
|
|
141
|
+
entries: [],
|
|
142
|
+
truncated: false
|
|
143
|
+
});
|
|
144
|
+
const newWorkspace = { ...workspaces[0], workspaceId: 'ws-new', title: 'foo' };
|
|
145
|
+
createWorkspaceMock.mockResolvedValue({
|
|
146
|
+
workspace: newWorkspace,
|
|
147
|
+
created: true
|
|
148
|
+
});
|
|
149
|
+
const onPick = vi.fn();
|
|
150
|
+
render(_jsx(WorkspaceView, { onPick: onPick }));
|
|
151
|
+
fireEvent.click(await screen.findByText('+ 新建工作区'));
|
|
152
|
+
const selectBtn = await screen.findByText('选择此目录');
|
|
153
|
+
fireEvent.click(selectBtn);
|
|
154
|
+
expect(await screen.findByText('创建中…')).toBeTruthy();
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
expect(createWorkspaceMock).toHaveBeenCalledWith('/home/user/projects/foo');
|
|
157
|
+
expect(onPick).toHaveBeenCalledWith(newWorkspace);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
it('handles errors from directory listing and workspace creation', async () => {
|
|
161
|
+
listWorkspacesMock.mockResolvedValue(workspaces);
|
|
162
|
+
listDirectoryMock.mockRejectedValue(new Error('Permission denied'));
|
|
163
|
+
const onPick = vi.fn();
|
|
164
|
+
render(_jsx(WorkspaceView, { onPick: onPick }));
|
|
165
|
+
fireEvent.click(await screen.findByText('+ 新建工作区'));
|
|
166
|
+
// Verify error from listing
|
|
167
|
+
expect(await screen.findByText('Permission denied')).toBeTruthy();
|
|
168
|
+
// Retry with success
|
|
169
|
+
listDirectoryMock.mockResolvedValue({
|
|
170
|
+
path: '/home',
|
|
171
|
+
home: '/home/user',
|
|
172
|
+
crumbs: [],
|
|
173
|
+
entries: [],
|
|
174
|
+
truncated: false
|
|
175
|
+
});
|
|
176
|
+
fireEvent.click(screen.getByText('重试'));
|
|
177
|
+
const selectBtn = await screen.findByText('选择此目录');
|
|
178
|
+
// Fail creation
|
|
179
|
+
createWorkspaceMock.mockRejectedValue(new Error('Already exists'));
|
|
180
|
+
fireEvent.click(selectBtn);
|
|
181
|
+
expect(await screen.findByText('Already exists')).toBeTruthy();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
* Security model:
|
|
10
10
|
* - Every request must carry a live paired-device cookie (the same gate
|
|
11
11
|
* semantic as the LAN fence), enforced before any host call.
|
|
12
|
-
* - Only an explicit allowlist of methods is proxied
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* - Only an explicit allowlist of methods is proxied ON THIS PREFIX. The
|
|
13
|
+
* allowlist constrains the /m/api proxy alone: the same paired-device
|
|
14
|
+
* cookie also passes the global api/gate, so a paired device is a
|
|
15
|
+
* full-control credential for the host /api surface outside the SDK's
|
|
16
|
+
* loopback-pinned privileged set (settings/credentials/agentPreset/host
|
|
17
|
+
* actions/llm.discoverModels). Pairing is full device trust.
|
|
15
18
|
* - `session.list` is paged here (the host API returns everything; this
|
|
16
19
|
* layer slices stable pages) so the phone never transfers the whole list.
|
|
17
20
|
* - The live mux stream is bridged over Server-Sent Events on the same
|
|
@@ -20,6 +23,7 @@
|
|
|
20
23
|
*/
|
|
21
24
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
22
25
|
import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy';
|
|
26
|
+
import type { PendingTracker } from './mobile-pending.ts';
|
|
23
27
|
import type { PairingService } from './pairing.ts';
|
|
24
28
|
/** Route-family dependencies. */
|
|
25
29
|
export interface MobileApiDeps {
|
|
@@ -27,6 +31,8 @@ export interface MobileApiDeps {
|
|
|
27
31
|
service: PairingService;
|
|
28
32
|
/** The host ApiProxy service (injected by the plugin). */
|
|
29
33
|
apiProxy: ApiProxy;
|
|
34
|
+
/** The pending tracker. */
|
|
35
|
+
pendingTracker: PendingTracker;
|
|
30
36
|
/** The resolved mobile composer preference (live per request). */
|
|
31
37
|
mobileEnterToSend: () => boolean;
|
|
32
38
|
/** SSE keep-alive ping cadence for the mux stream (default 15000 ms; test seam). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;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,qBAAqB,CAAA;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AA6DlD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,CAAA;IAClB,2BAA2B;IAC3B,cAAc,EAAE,cAAc,CAAA;IAC9B,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,OAAO,CAAA;IAChC,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,8BAA8B;AAC9B,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAOV;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA+KnE"}
|
package/lib/types/mobile-api.js
CHANGED
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
* Security model:
|
|
10
10
|
* - Every request must carry a live paired-device cookie (the same gate
|
|
11
11
|
* semantic as the LAN fence), enforced before any host call.
|
|
12
|
-
* - Only an explicit allowlist of methods is proxied
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* - Only an explicit allowlist of methods is proxied ON THIS PREFIX. The
|
|
13
|
+
* allowlist constrains the /m/api proxy alone: the same paired-device
|
|
14
|
+
* cookie also passes the global api/gate, so a paired device is a
|
|
15
|
+
* full-control credential for the host /api surface outside the SDK's
|
|
16
|
+
* loopback-pinned privileged set (settings/credentials/agentPreset/host
|
|
17
|
+
* actions/llm.discoverModels). Pairing is full device trust.
|
|
15
18
|
* - `session.list` is paged here (the host API returns everything; this
|
|
16
19
|
* layer slices stable pages) so the phone never transfers the whole list.
|
|
17
20
|
* - The live mux stream is bridged over Server-Sent Events on the same
|
|
@@ -30,6 +33,8 @@ import { readCookie } from "./gate.js";
|
|
|
30
33
|
* device at a time.
|
|
31
34
|
*/
|
|
32
35
|
const MOBILE_ALLOWLIST = new Set([
|
|
36
|
+
'host.listDirectory',
|
|
37
|
+
'workspace.create',
|
|
33
38
|
'workspace.list',
|
|
34
39
|
'agentPreset.list',
|
|
35
40
|
'session.create',
|
|
@@ -47,6 +52,8 @@ const MOBILE_ALLOWLIST = new Set([
|
|
|
47
52
|
* settings-domain write).
|
|
48
53
|
*/
|
|
49
54
|
const MOBILE_PREFERENCES_METHOD = 'mobile.preferences';
|
|
55
|
+
const MOBILE_PENDING_METHOD = 'mobile.pending';
|
|
56
|
+
const MOBILE_RESPOND_METHOD = 'mobile.respond';
|
|
50
57
|
/** One session.list page (thin phones load incrementally). */
|
|
51
58
|
const SESSION_PAGE_SIZE = 20;
|
|
52
59
|
/** SSE keep-alive ping cadence for the live mux stream (single connection). */
|
|
@@ -120,14 +127,16 @@ export function makeMobileApiRoutes(deps) {
|
|
|
120
127
|
return;
|
|
121
128
|
}
|
|
122
129
|
const method = pathname.slice(MOBILE_API_METHOD_PREFIX.length);
|
|
123
|
-
const local = method === MOBILE_PREFERENCES_METHOD
|
|
130
|
+
const local = method === MOBILE_PREFERENCES_METHOD
|
|
131
|
+
|| method === MOBILE_PENDING_METHOD
|
|
132
|
+
|| method === MOBILE_RESPOND_METHOD;
|
|
124
133
|
if (!MOBILE_ALLOWLIST.has(method) && !local) {
|
|
125
134
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: `method ${method} is not exposed to the mobile surface` } });
|
|
126
135
|
return;
|
|
127
136
|
}
|
|
128
137
|
let envelope;
|
|
129
138
|
try {
|
|
130
|
-
envelope = await
|
|
139
|
+
envelope = await readBoundedJson(req, 64 * 1024);
|
|
131
140
|
}
|
|
132
141
|
catch {
|
|
133
142
|
writeJson(res, 400, { ok: false, error: { code: 'bad-request', message: 'invalid json body' } });
|
|
@@ -140,11 +149,44 @@ export function makeMobileApiRoutes(deps) {
|
|
|
140
149
|
return;
|
|
141
150
|
}
|
|
142
151
|
if (local) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
if (method === MOBILE_PREFERENCES_METHOD) {
|
|
153
|
+
writeJson(res, 200, {
|
|
154
|
+
type: 'server-response',
|
|
155
|
+
rpcId,
|
|
156
|
+
result: { ok: true, value: { mobileEnterToSend: mobileEnterToSend() } },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
else if (method === MOBILE_PENDING_METHOD) {
|
|
160
|
+
const payload = parsed.payload;
|
|
161
|
+
writeJson(res, 200, {
|
|
162
|
+
type: 'server-response',
|
|
163
|
+
rpcId,
|
|
164
|
+
result: { ok: true, value: deps.pendingTracker.pending(payload?.sessionId) },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
else if (method === MOBILE_RESPOND_METHOD) {
|
|
168
|
+
const payload = parsed.payload;
|
|
169
|
+
try {
|
|
170
|
+
const receipt = await apiProxy.respond({
|
|
171
|
+
type: 'client-response',
|
|
172
|
+
rpcId: RpcId(payload.rpcId),
|
|
173
|
+
result: { ok: true, value: payload.response },
|
|
174
|
+
});
|
|
175
|
+
writeJson(res, 200, {
|
|
176
|
+
type: 'server-response',
|
|
177
|
+
rpcId,
|
|
178
|
+
result: { ok: true, value: receipt },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
183
|
+
writeJson(res, 200, {
|
|
184
|
+
type: 'server-response',
|
|
185
|
+
rpcId,
|
|
186
|
+
result: { ok: false, error: { code: 'internal', message } },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
148
190
|
return;
|
|
149
191
|
}
|
|
150
192
|
try {
|
|
@@ -173,8 +215,10 @@ export function makeMobileApiRoutes(deps) {
|
|
|
173
215
|
return;
|
|
174
216
|
}
|
|
175
217
|
if (!gateOk(req)) {
|
|
176
|
-
res
|
|
177
|
-
|
|
218
|
+
writeJson(res, 403, {
|
|
219
|
+
ok: false,
|
|
220
|
+
error: { code: 'unpaired', message: 'mobile session is not paired' },
|
|
221
|
+
});
|
|
178
222
|
return;
|
|
179
223
|
}
|
|
180
224
|
res.writeHead(200, {
|
|
@@ -212,6 +256,7 @@ export function makeMobileApiRoutes(deps) {
|
|
|
212
256
|
for await (const frame of frames) {
|
|
213
257
|
if (closed)
|
|
214
258
|
break;
|
|
259
|
+
deps.pendingTracker.onFrame(frame);
|
|
215
260
|
res.write(`data: ${JSON.stringify(frame)}\n\n`);
|
|
216
261
|
}
|
|
217
262
|
}
|
|
@@ -230,10 +275,6 @@ export function makeMobileApiRoutes(deps) {
|
|
|
230
275
|
{ kind: 'exact', path: MOBILE_API_PATHS.events, handler: handleEvents },
|
|
231
276
|
];
|
|
232
277
|
}
|
|
233
|
-
/** Read a request body as JSON (bounded). */
|
|
234
|
-
async function readJsonBody(req) {
|
|
235
|
-
return readBoundedJson(req, 64 * 1024);
|
|
236
|
-
}
|
|
237
278
|
/** Dispatch one allowlisted method through the host ApiProxy. */
|
|
238
279
|
async function dispatch(apiProxy, method, payload, rpcId, signal) {
|
|
239
280
|
const request = { rpcId: RpcId(rpcId), payload };
|
|
@@ -284,6 +325,10 @@ async function dispatch(apiProxy, method, payload, rpcId, signal) {
|
|
|
284
325
|
});
|
|
285
326
|
if (method === 'workspace.list')
|
|
286
327
|
return wrap(await apiProxy.workspace.list(request));
|
|
328
|
+
if (method === 'workspace.create')
|
|
329
|
+
return wrap(await apiProxy.workspace.create(request));
|
|
330
|
+
if (method === 'host.listDirectory')
|
|
331
|
+
return wrap(await apiProxy.host.listDirectory(request, signal ?? new AbortController().signal));
|
|
287
332
|
if (method === 'agentPreset.list')
|
|
288
333
|
return wrap(await apiProxy.agentPresets.list(request));
|
|
289
334
|
if (method === 'session.create')
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { MuxFrame } from '@deepseek-ai/dsh-host-apiproxy/api/events';
|
|
2
|
+
import type { RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api/rpc';
|
|
3
|
+
export interface PendingApproval {
|
|
4
|
+
rpcId: string;
|
|
5
|
+
approvalId: string;
|
|
6
|
+
toolName: string;
|
|
7
|
+
callId?: string;
|
|
8
|
+
reason?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface PendingQuestion {
|
|
11
|
+
rpcId: string;
|
|
12
|
+
questions: Array<{
|
|
13
|
+
id: string;
|
|
14
|
+
question: string;
|
|
15
|
+
detail?: string;
|
|
16
|
+
header?: string;
|
|
17
|
+
options?: Array<{
|
|
18
|
+
label: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
}>;
|
|
21
|
+
multiSelect?: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export interface PendingState {
|
|
25
|
+
approvals: PendingApproval[];
|
|
26
|
+
questions: PendingQuestion[];
|
|
27
|
+
}
|
|
28
|
+
export declare class PendingTracker {
|
|
29
|
+
private readonly sessions;
|
|
30
|
+
/** Process one mux frame and update the pending state. */
|
|
31
|
+
onFrame(frame: RpcRequest<MuxFrame>): void;
|
|
32
|
+
private _getOrInit;
|
|
33
|
+
/** Query pending items for a session. */
|
|
34
|
+
pending(sessionId: string): PendingState;
|
|
35
|
+
/** Clear all pending state for a session. */
|
|
36
|
+
clear(sessionId: string): void;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=mobile-pending.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mobile-pending.d.ts","sourceRoot":"","sources":["../../src/mobile-pending.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2CAA2C,CAAA;AACzE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wCAAwC,CAAA;AAExE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,KAAK,CAAC;QACf,EAAE,EAAE,MAAM,CAAA;QACV,QAAQ,EAAE,MAAM,CAAA;QAChB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,OAAO,CAAC,EAAE,KAAK,CAAC;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QACxD,WAAW,CAAC,EAAE,OAAO,CAAA;KACtB,CAAC,CAAA;CACH;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,eAAe,EAAE,CAAA;IAC5B,SAAS,EAAE,eAAe,EAAE,CAAA;CAC7B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA+F;IAExH,0DAA0D;IAC1D,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC,GAAG,IAAI;IA8B1C,OAAO,CAAC,UAAU;IASlB,yCAAyC;IACzC,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY;IAWxC,6CAA6C;IAC7C,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;CAG/B"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export class PendingTracker {
|
|
2
|
+
sessions = new Map();
|
|
3
|
+
/** Process one mux frame and update the pending state. */
|
|
4
|
+
onFrame(frame) {
|
|
5
|
+
const payload = frame.payload;
|
|
6
|
+
if (payload.type === 'approval/requested') {
|
|
7
|
+
const state = this._getOrInit(payload.sessionId);
|
|
8
|
+
state.approvals.set(payload.approvalId, {
|
|
9
|
+
rpcId: frame.rpcId,
|
|
10
|
+
approvalId: payload.approvalId,
|
|
11
|
+
toolName: payload.toolName,
|
|
12
|
+
callId: payload.callId,
|
|
13
|
+
reason: payload.reason,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
else if (payload.type === 'approval/resolved') {
|
|
17
|
+
const state = this.sessions.get(payload.sessionId);
|
|
18
|
+
if (state) {
|
|
19
|
+
state.approvals.delete(payload.approvalId);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else if (payload.type === 'question/requested') {
|
|
23
|
+
const state = this._getOrInit(payload.sessionId);
|
|
24
|
+
state.questions.push({
|
|
25
|
+
rpcId: frame.rpcId,
|
|
26
|
+
questions: payload.questions,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else if (payload.type === 'question/resolved') {
|
|
30
|
+
const state = this.sessions.get(payload.sessionId);
|
|
31
|
+
if (state) {
|
|
32
|
+
state.questions = state.questions.filter(q => q.rpcId !== payload.questionRpcId);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
_getOrInit(sessionId) {
|
|
37
|
+
let state = this.sessions.get(sessionId);
|
|
38
|
+
if (!state) {
|
|
39
|
+
state = { approvals: new Map(), questions: [] };
|
|
40
|
+
this.sessions.set(sessionId, state);
|
|
41
|
+
}
|
|
42
|
+
return state;
|
|
43
|
+
}
|
|
44
|
+
/** Query pending items for a session. */
|
|
45
|
+
pending(sessionId) {
|
|
46
|
+
const state = this.sessions.get(sessionId);
|
|
47
|
+
if (!state) {
|
|
48
|
+
return { approvals: [], questions: [] };
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
approvals: Array.from(state.approvals.values()),
|
|
52
|
+
questions: state.questions,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Clear all pending state for a session. */
|
|
56
|
+
clear(sessionId) {
|
|
57
|
+
this.sessions.delete(sessionId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy/api';
|
|
2
|
+
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
3
|
+
import type { PairingService } from './pairing.ts';
|
|
4
|
+
export declare const PAIRED_MODEL_CATALOG_PATHS: {
|
|
5
|
+
readonly catalog: "/api/pair/model-catalog";
|
|
6
|
+
readonly discover: "/api/pair/model-catalog/discover";
|
|
7
|
+
readonly upsert: "/api/pair/model-catalog/upsert";
|
|
8
|
+
};
|
|
9
|
+
export interface PairedModelCatalogDeps {
|
|
10
|
+
service: PairingService;
|
|
11
|
+
apiProxy: ApiProxy;
|
|
12
|
+
/** The LAN IP literals the host fence accepts, mirroring the /api/pair fence. */
|
|
13
|
+
lanAddresses: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
/** Build the narrow paired-device model catalog routes. */
|
|
16
|
+
export declare function makePairedModelCatalogRoutes(deps: PairedModelCatalogDeps): WebRoute[];
|
|
17
|
+
//# sourceMappingURL=paired-model-catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paired-model-catalog.d.ts","sourceRoot":"","sources":["../../src/paired-model-catalog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAiE,MAAM,oCAAoC,CAAA;AACjI,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAE/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AA6BlD,eAAO,MAAM,0BAA0B;;;;CAI7B,CAAA;AAEV,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,cAAc,CAAA;IACvB,QAAQ,EAAE,QAAQ,CAAA;IAClB,iFAAiF;IACjF,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;CAChC;AAsGD,2DAA2D;AAC3D,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,sBAAsB,GAAG,QAAQ,EAAE,CA2ErF"}
|