@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
|
@@ -7,10 +7,125 @@
|
|
|
7
7
|
|
|
8
8
|
import { useEffect, useState } from 'react'
|
|
9
9
|
import type { WorkspaceView as WorkspaceRow } from '@deepseek-ai/dsh-host-apiproxy/api/workspace'
|
|
10
|
-
import { listWorkspaces } from '../api.ts'
|
|
10
|
+
import { listWorkspaces, listDirectory, createWorkspace, type DirectoryListing, type DirectoryEntry } from '../api.ts'
|
|
11
11
|
import { errorText } from './App.tsx'
|
|
12
12
|
import { ThemeToggle } from '../theme-toggle.tsx'
|
|
13
13
|
|
|
14
|
+
/** Props for the directory browser. */
|
|
15
|
+
interface DirectoryBrowserProps {
|
|
16
|
+
onCancel(): void
|
|
17
|
+
onPick(workspace: WorkspaceRow): void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Render the directory browser for creating a new workspace.
|
|
22
|
+
*/
|
|
23
|
+
function DirectoryBrowser({ onCancel, onPick }: DirectoryBrowserProps) {
|
|
24
|
+
const [listing, setListing] = useState<DirectoryListing | undefined>(undefined)
|
|
25
|
+
const [error, setError] = useState<string | undefined>(undefined)
|
|
26
|
+
const [creating, setCreating] = useState(false)
|
|
27
|
+
const [path, setPath] = useState<string | undefined>(undefined)
|
|
28
|
+
const [reload, setReload] = useState(0)
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
let cancelled = false
|
|
32
|
+
setError(undefined)
|
|
33
|
+
setListing(undefined)
|
|
34
|
+
void listDirectory(path).then(
|
|
35
|
+
(res) => {
|
|
36
|
+
if (cancelled) return
|
|
37
|
+
setListing(res)
|
|
38
|
+
},
|
|
39
|
+
(reason: unknown) => {
|
|
40
|
+
if (cancelled) return
|
|
41
|
+
setError(errorText(reason))
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
return () => { cancelled = true }
|
|
45
|
+
}, [path, reload])
|
|
46
|
+
|
|
47
|
+
const handleCreate = async () => {
|
|
48
|
+
if (listing === undefined || creating) return
|
|
49
|
+
setCreating(true)
|
|
50
|
+
setError(undefined)
|
|
51
|
+
try {
|
|
52
|
+
const result = await createWorkspace(listing.path)
|
|
53
|
+
onPick(result.workspace)
|
|
54
|
+
} catch (reason: unknown) {
|
|
55
|
+
setCreating(false)
|
|
56
|
+
setError(errorText(reason))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="mobile dir-browser">
|
|
62
|
+
<header className="mobile-header">
|
|
63
|
+
<button type="button" className="mobile-headerAction" onClick={onCancel}>返回</button>
|
|
64
|
+
<h1 className="mobile-title">选择目录</h1>
|
|
65
|
+
</header>
|
|
66
|
+
|
|
67
|
+
{listing !== undefined && (
|
|
68
|
+
<div className="dir-crumbs">
|
|
69
|
+
{listing.crumbs.map((crumb, idx) => (
|
|
70
|
+
<span key={crumb.path}>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
className="dir-crumb"
|
|
74
|
+
onClick={() => setPath(crumb.path)}
|
|
75
|
+
>
|
|
76
|
+
{crumb.name || '/'}
|
|
77
|
+
</button>
|
|
78
|
+
{idx < listing.crumbs.length - 1 && <span className="dir-crumb-separator">/</span>}
|
|
79
|
+
</span>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
{error !== undefined ? (
|
|
85
|
+
<div className="mobile-empty">
|
|
86
|
+
<p className="mobile-error">{error}</p>
|
|
87
|
+
<button type="button" className="mobile-button" onClick={() => setReload(n => n + 1)}>重试</button>
|
|
88
|
+
</div>
|
|
89
|
+
) : listing === undefined ? (
|
|
90
|
+
<div className="mobile-empty">
|
|
91
|
+
<p className="mobile-muted">加载中…</p>
|
|
92
|
+
</div>
|
|
93
|
+
) : (
|
|
94
|
+
<ul className="mobile-list">
|
|
95
|
+
{listing.entries.length === 0 ? (
|
|
96
|
+
<div className="mobile-empty dir-empty">
|
|
97
|
+
<p className="mobile-muted">空目录</p>
|
|
98
|
+
</div>
|
|
99
|
+
) : (
|
|
100
|
+
listing.entries.map((entry: DirectoryEntry) => (
|
|
101
|
+
<li key={entry.path}>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
className={`mobile-row dir-entry ${entry.hidden ? 'dir-entry-hidden' : ''}`}
|
|
105
|
+
onClick={() => setPath(entry.path)}
|
|
106
|
+
>
|
|
107
|
+
<span className="mobile-rowTitle">📁 {entry.name}</span>
|
|
108
|
+
</button>
|
|
109
|
+
</li>
|
|
110
|
+
))
|
|
111
|
+
)}
|
|
112
|
+
</ul>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
<div className="dir-select">
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
className="mobile-button"
|
|
119
|
+
disabled={listing === undefined || creating}
|
|
120
|
+
onClick={() => void handleCreate()}
|
|
121
|
+
>
|
|
122
|
+
{creating ? '创建中…' : '选择此目录'}
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
14
129
|
/** Props for the workspace roster. */
|
|
15
130
|
export interface WorkspaceViewProps {
|
|
16
131
|
/** Workspace carried by the pairing link; opened after the roster loads. */
|
|
@@ -29,6 +144,7 @@ export function WorkspaceView({ initialWorkspaceId, onPick }: WorkspaceViewProps
|
|
|
29
144
|
const [error, setError] = useState<string | undefined>(undefined)
|
|
30
145
|
// Bumped by the retry button to re-run the roster fetch effect.
|
|
31
146
|
const [reload, setReload] = useState(0)
|
|
147
|
+
const [isCreating, setIsCreating] = useState(false)
|
|
32
148
|
|
|
33
149
|
useEffect(() => {
|
|
34
150
|
let cancelled = false
|
|
@@ -52,6 +168,15 @@ export function WorkspaceView({ initialWorkspaceId, onPick }: WorkspaceViewProps
|
|
|
52
168
|
return () => { cancelled = true }
|
|
53
169
|
}, [initialWorkspaceId, onPick, reload])
|
|
54
170
|
|
|
171
|
+
if (isCreating) {
|
|
172
|
+
return (
|
|
173
|
+
<DirectoryBrowser
|
|
174
|
+
onCancel={() => setIsCreating(false)}
|
|
175
|
+
onPick={onPick}
|
|
176
|
+
/>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
55
180
|
if (error !== undefined) {
|
|
56
181
|
return (
|
|
57
182
|
<div className="mobile">
|
|
@@ -83,37 +208,38 @@ export function WorkspaceView({ initialWorkspaceId, onPick }: WorkspaceViewProps
|
|
|
83
208
|
)
|
|
84
209
|
}
|
|
85
210
|
|
|
86
|
-
if (items.length === 0) {
|
|
87
|
-
return (
|
|
88
|
-
<div className="mobile">
|
|
89
|
-
<header className="mobile-header">
|
|
90
|
-
<h1 className="mobile-title">工作区</h1>
|
|
91
|
-
<ThemeToggle />
|
|
92
|
-
</header>
|
|
93
|
-
<div className="mobile-empty">
|
|
94
|
-
<p className="mobile-muted">暂无工作区</p>
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
211
|
return (
|
|
101
212
|
<div className="mobile">
|
|
102
213
|
<header className="mobile-header">
|
|
103
214
|
<h1 className="mobile-title">工作区</h1>
|
|
104
215
|
<ThemeToggle />
|
|
105
216
|
</header>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
217
|
+
{items.length === 0 ? (
|
|
218
|
+
<div className="mobile-empty">
|
|
219
|
+
<p className="mobile-muted">暂无工作区</p>
|
|
220
|
+
</div>
|
|
221
|
+
) : (
|
|
222
|
+
<ul className="mobile-list">
|
|
223
|
+
{items.map(workspace => (
|
|
224
|
+
<li key={workspace.workspaceId}>
|
|
225
|
+
<button type="button" className="mobile-row" onClick={() => { onPick(workspace) }}>
|
|
226
|
+
<span className="mobile-rowTitle">{workspace.title}</span>
|
|
227
|
+
<span className="mobile-rowMeta">{workspace.path}</span>
|
|
228
|
+
<span className="mobile-chevron">›</span>
|
|
229
|
+
</button>
|
|
230
|
+
</li>
|
|
231
|
+
))}
|
|
232
|
+
</ul>
|
|
233
|
+
)}
|
|
234
|
+
<div style={{ padding: '16px' }}>
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
className="mobile-button"
|
|
238
|
+
onClick={() => setIsCreating(true)}
|
|
239
|
+
>
|
|
240
|
+
+ 新建工作区
|
|
241
|
+
</button>
|
|
242
|
+
</div>
|
|
117
243
|
</div>
|
|
118
244
|
)
|
|
119
245
|
}
|
package/src/mobile-api.ts
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
|
|
@@ -24,6 +27,7 @@ import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
|
|
24
27
|
import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy'
|
|
25
28
|
import type { RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
|
|
26
29
|
import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
|
|
30
|
+
import type { PendingTracker } from './mobile-pending.ts'
|
|
27
31
|
import type { PairingService } from './pairing.ts'
|
|
28
32
|
import { readBoundedJson, writeJson } from './http.ts'
|
|
29
33
|
import { readCookie } from './gate.ts'
|
|
@@ -37,6 +41,8 @@ import { readCookie } from './gate.ts'
|
|
|
37
41
|
* device at a time.
|
|
38
42
|
*/
|
|
39
43
|
const MOBILE_ALLOWLIST = new Set([
|
|
44
|
+
'host.listDirectory',
|
|
45
|
+
'workspace.create',
|
|
40
46
|
'workspace.list',
|
|
41
47
|
'agentPreset.list',
|
|
42
48
|
'session.create',
|
|
@@ -55,6 +61,8 @@ const MOBILE_ALLOWLIST = new Set([
|
|
|
55
61
|
* settings-domain write).
|
|
56
62
|
*/
|
|
57
63
|
const MOBILE_PREFERENCES_METHOD = 'mobile.preferences'
|
|
64
|
+
const MOBILE_PENDING_METHOD = 'mobile.pending'
|
|
65
|
+
const MOBILE_RESPOND_METHOD = 'mobile.respond'
|
|
58
66
|
|
|
59
67
|
/** One session.list page (thin phones load incrementally). */
|
|
60
68
|
const SESSION_PAGE_SIZE = 20
|
|
@@ -87,6 +95,8 @@ export interface MobileApiDeps {
|
|
|
87
95
|
service: PairingService
|
|
88
96
|
/** The host ApiProxy service (injected by the plugin). */
|
|
89
97
|
apiProxy: ApiProxy
|
|
98
|
+
/** The pending tracker. */
|
|
99
|
+
pendingTracker: PendingTracker
|
|
90
100
|
/** The resolved mobile composer preference (live per request). */
|
|
91
101
|
mobileEnterToSend: () => boolean
|
|
92
102
|
/** SSE keep-alive ping cadence for the mux stream (default 15000 ms; test seam). */
|
|
@@ -147,14 +157,16 @@ export function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[] {
|
|
|
147
157
|
return
|
|
148
158
|
}
|
|
149
159
|
const method = pathname.slice(MOBILE_API_METHOD_PREFIX.length)
|
|
150
|
-
const local = method === MOBILE_PREFERENCES_METHOD
|
|
160
|
+
const local = method === MOBILE_PREFERENCES_METHOD
|
|
161
|
+
|| method === MOBILE_PENDING_METHOD
|
|
162
|
+
|| method === MOBILE_RESPOND_METHOD
|
|
151
163
|
if (!MOBILE_ALLOWLIST.has(method) && !local) {
|
|
152
164
|
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: `method ${method} is not exposed to the mobile surface` } })
|
|
153
165
|
return
|
|
154
166
|
}
|
|
155
167
|
let envelope: unknown
|
|
156
168
|
try {
|
|
157
|
-
envelope = await
|
|
169
|
+
envelope = await readBoundedJson(req, 64 * 1024)
|
|
158
170
|
} catch {
|
|
159
171
|
writeJson(res, 400, { ok: false, error: { code: 'bad-request', message: 'invalid json body' } })
|
|
160
172
|
return
|
|
@@ -166,11 +178,41 @@ export function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[] {
|
|
|
166
178
|
return
|
|
167
179
|
}
|
|
168
180
|
if (local) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
181
|
+
if (method === MOBILE_PREFERENCES_METHOD) {
|
|
182
|
+
writeJson(res, 200, {
|
|
183
|
+
type: 'server-response',
|
|
184
|
+
rpcId,
|
|
185
|
+
result: { ok: true, value: { mobileEnterToSend: mobileEnterToSend() } },
|
|
186
|
+
})
|
|
187
|
+
} else if (method === MOBILE_PENDING_METHOD) {
|
|
188
|
+
const payload = parsed.payload as any
|
|
189
|
+
writeJson(res, 200, {
|
|
190
|
+
type: 'server-response',
|
|
191
|
+
rpcId,
|
|
192
|
+
result: { ok: true, value: deps.pendingTracker.pending(payload?.sessionId) },
|
|
193
|
+
})
|
|
194
|
+
} else if (method === MOBILE_RESPOND_METHOD) {
|
|
195
|
+
const payload = parsed.payload as any
|
|
196
|
+
try {
|
|
197
|
+
const receipt = await apiProxy.respond({
|
|
198
|
+
type: 'client-response',
|
|
199
|
+
rpcId: RpcId(payload.rpcId),
|
|
200
|
+
result: { ok: true, value: payload.response },
|
|
201
|
+
})
|
|
202
|
+
writeJson(res, 200, {
|
|
203
|
+
type: 'server-response',
|
|
204
|
+
rpcId,
|
|
205
|
+
result: { ok: true, value: receipt },
|
|
206
|
+
})
|
|
207
|
+
} catch (error) {
|
|
208
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
209
|
+
writeJson(res, 200, {
|
|
210
|
+
type: 'server-response',
|
|
211
|
+
rpcId,
|
|
212
|
+
result: { ok: false, error: { code: 'internal', message } },
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
}
|
|
174
216
|
return
|
|
175
217
|
}
|
|
176
218
|
try {
|
|
@@ -198,8 +240,10 @@ export function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[] {
|
|
|
198
240
|
return
|
|
199
241
|
}
|
|
200
242
|
if (!gateOk(req)) {
|
|
201
|
-
res
|
|
202
|
-
|
|
243
|
+
writeJson(res, 403, {
|
|
244
|
+
ok: false,
|
|
245
|
+
error: { code: 'unpaired', message: 'mobile session is not paired' },
|
|
246
|
+
})
|
|
203
247
|
return
|
|
204
248
|
}
|
|
205
249
|
res.writeHead(200, {
|
|
@@ -233,6 +277,7 @@ export function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[] {
|
|
|
233
277
|
const frames = apiProxy.events.mux({ rpcId: RpcId(`mobile-mux-${Date.now().toString(36)}`), payload: {} }, controller.signal)
|
|
234
278
|
for await (const frame of frames) {
|
|
235
279
|
if (closed) break
|
|
280
|
+
deps.pendingTracker.onFrame(frame as any)
|
|
236
281
|
res.write(`data: ${JSON.stringify(frame)}\n\n`)
|
|
237
282
|
}
|
|
238
283
|
} catch {
|
|
@@ -250,11 +295,6 @@ export function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[] {
|
|
|
250
295
|
]
|
|
251
296
|
}
|
|
252
297
|
|
|
253
|
-
/** Read a request body as JSON (bounded). */
|
|
254
|
-
async function readJsonBody(req: IncomingMessage): Promise<unknown> {
|
|
255
|
-
return readBoundedJson(req, 64 * 1024)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
298
|
/** Dispatch one allowlisted method through the host ApiProxy. */
|
|
259
299
|
async function dispatch(apiProxy: ApiProxy, method: string, payload: unknown, rpcId: string, signal?: AbortSignal): Promise<unknown> {
|
|
260
300
|
const request: RpcRequest<unknown> = { rpcId: RpcId(rpcId), payload }
|
|
@@ -303,6 +343,8 @@ async function dispatch(apiProxy: ApiProxy, method: string, payload: unknown, rp
|
|
|
303
343
|
result: response.result,
|
|
304
344
|
})
|
|
305
345
|
if (method === 'workspace.list') return wrap(await apiProxy.workspace.list(request as never))
|
|
346
|
+
if (method === 'workspace.create') return wrap(await apiProxy.workspace.create(request as never))
|
|
347
|
+
if (method === 'host.listDirectory') return wrap(await apiProxy.host.listDirectory(request as never, signal ?? new AbortController().signal))
|
|
306
348
|
if (method === 'agentPreset.list') return wrap(await apiProxy.agentPresets.list(request as never))
|
|
307
349
|
if (method === 'session.create') return wrap(await apiProxy.sessions.create(request as never))
|
|
308
350
|
if (method === 'session.history') return wrap(await apiProxy.sessions.history(request as never))
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
|
|
4
|
+
export interface PendingApproval {
|
|
5
|
+
rpcId: string
|
|
6
|
+
approvalId: string
|
|
7
|
+
toolName: string
|
|
8
|
+
callId?: string
|
|
9
|
+
reason?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PendingQuestion {
|
|
13
|
+
rpcId: string
|
|
14
|
+
questions: Array<{
|
|
15
|
+
id: string
|
|
16
|
+
question: string
|
|
17
|
+
detail?: string
|
|
18
|
+
header?: string
|
|
19
|
+
options?: Array<{ label: string; description?: string }>
|
|
20
|
+
multiSelect?: boolean
|
|
21
|
+
}>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PendingState {
|
|
25
|
+
approvals: PendingApproval[]
|
|
26
|
+
questions: PendingQuestion[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class PendingTracker {
|
|
30
|
+
private readonly sessions = new Map<string, { approvals: Map<string, PendingApproval>; questions: PendingQuestion[] }>()
|
|
31
|
+
|
|
32
|
+
/** Process one mux frame and update the pending state. */
|
|
33
|
+
onFrame(frame: RpcRequest<MuxFrame>): void {
|
|
34
|
+
const payload = frame.payload
|
|
35
|
+
if (payload.type === 'approval/requested') {
|
|
36
|
+
const state = this._getOrInit(payload.sessionId)
|
|
37
|
+
state.approvals.set(payload.approvalId, {
|
|
38
|
+
rpcId: frame.rpcId,
|
|
39
|
+
approvalId: payload.approvalId,
|
|
40
|
+
toolName: payload.toolName,
|
|
41
|
+
callId: payload.callId,
|
|
42
|
+
reason: payload.reason,
|
|
43
|
+
})
|
|
44
|
+
} else if (payload.type === 'approval/resolved') {
|
|
45
|
+
const state = this.sessions.get(payload.sessionId)
|
|
46
|
+
if (state) {
|
|
47
|
+
state.approvals.delete(payload.approvalId)
|
|
48
|
+
}
|
|
49
|
+
} else if (payload.type === 'question/requested') {
|
|
50
|
+
const state = this._getOrInit(payload.sessionId)
|
|
51
|
+
state.questions.push({
|
|
52
|
+
rpcId: frame.rpcId,
|
|
53
|
+
questions: payload.questions as any,
|
|
54
|
+
})
|
|
55
|
+
} else if (payload.type === 'question/resolved') {
|
|
56
|
+
const state = this.sessions.get(payload.sessionId)
|
|
57
|
+
if (state) {
|
|
58
|
+
state.questions = state.questions.filter(q => q.rpcId !== payload.questionRpcId)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private _getOrInit(sessionId: string) {
|
|
64
|
+
let state = this.sessions.get(sessionId)
|
|
65
|
+
if (!state) {
|
|
66
|
+
state = { approvals: new Map(), questions: [] }
|
|
67
|
+
this.sessions.set(sessionId, state)
|
|
68
|
+
}
|
|
69
|
+
return state
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Query pending items for a session. */
|
|
73
|
+
pending(sessionId: string): PendingState {
|
|
74
|
+
const state = this.sessions.get(sessionId)
|
|
75
|
+
if (!state) {
|
|
76
|
+
return { approvals: [], questions: [] }
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
approvals: Array.from(state.approvals.values()),
|
|
80
|
+
questions: state.questions,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Clear all pending state for a session. */
|
|
85
|
+
clear(sessionId: string): void {
|
|
86
|
+
this.sessions.delete(sessionId)
|
|
87
|
+
}
|
|
88
|
+
}
|