@morlay/dsh-desktop-host 0.2.5 → 0.2.7
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.md +20 -6
- package/config/desktop.cordis.patch.yml +37 -35
- package/dist/index.d.mts +1 -0
- package/dist/index.js +430 -0
- package/dist/paths.d.mts +4 -0
- package/dist/paths.js +5 -0
- package/dist/preload-source.d.mts +29 -0
- package/dist/preload-source.js +80 -0
- package/dist/settings-namespace.d.mts +9 -0
- package/dist/settings-namespace.js +13 -0
- package/dist/wire.d.mts +109 -0
- package/{lib → dist}/wire.js +4 -1
- package/package.json +41 -17
- package/src/__tests__/desktop-patch.spec.ts +171 -0
- package/src/__tests__/inspector.spec.ts +50 -0
- package/src/__tests__/invocation.spec.ts +60 -0
- package/src/__tests__/manifest.spec.ts +70 -0
- package/src/__tests__/web-entry.spec.ts +13 -0
- package/src/__tests__/wire.spec.ts +238 -0
- package/src/desktop-ipc.ts +71 -0
- package/src/fatal.ts +30 -0
- package/src/index.ts +126 -0
- package/src/inspector.ts +44 -0
- package/src/invocation.ts +68 -0
- package/src/paths.ts +4 -0
- package/src/preload-source.ts +86 -0
- package/src/settings-namespace.ts +31 -0
- package/src/transport.ts +286 -0
- package/src/web.ts +57 -0
- package/src/wire.ts +480 -0
- package/lib/index.js +0 -390
- package/lib/webserver.js +0 -250
package/lib/index.js
DELETED
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
import { DESKTOP_PIPE_CHUNK_BYTES, DESKTOP_STREAM_PATH, DesktopHostRequestDecoder, DesktopStreamBodyDecoder, encodeDesktopResponseData, encodeDesktopResponseEnd, encodeDesktopResponseError, encodeDesktopResponseStart, isDesktopHostCommand } from "./wire.js";
|
|
2
|
-
import { once } from "node:events";
|
|
3
|
-
import { createReadStream, createWriteStream } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { inspect } from "node:util";
|
|
7
|
-
import { loadLayeredEnv, loadProfileDirectory, reportSkippedBundles } from "@deepseek-ai/dsh-app-boot";
|
|
8
|
-
import { runProfile } from "@deepseek-ai/dsh/profile-boot";
|
|
9
|
-
//#region src/transport.ts
|
|
10
|
-
const DESKTOP_TRANSPORT_SCRIPT = `globalThis.__DSH_TRANSPORT__={
|
|
11
|
-
_desktop:true,
|
|
12
|
-
ownsHost:true,
|
|
13
|
-
async *openStream(endpoint,payload,signal,uplink){
|
|
14
|
-
console.info('[dsh-desktop] openStream',endpoint)
|
|
15
|
-
const carrier=globalThis.__DSH_DESKTOP_STREAM__
|
|
16
|
-
if(carrier===undefined){
|
|
17
|
-
console.error('[dsh-desktop] stream carrier missing for',endpoint)
|
|
18
|
-
throw new Error('desktop stream carrier is unavailable')
|
|
19
|
-
}
|
|
20
|
-
const queue=[]
|
|
21
|
-
let buffer=''
|
|
22
|
-
let ended=false
|
|
23
|
-
let failure
|
|
24
|
-
let wake
|
|
25
|
-
const notify=()=>{const pending=wake;wake=undefined;if(pending)pending()}
|
|
26
|
-
const append=(text)=>{
|
|
27
|
-
buffer+=text
|
|
28
|
-
let at
|
|
29
|
-
while((at=buffer.indexOf('\\n'))!==-1){
|
|
30
|
-
const line=buffer.slice(0,at)
|
|
31
|
-
buffer=buffer.slice(at+1)
|
|
32
|
-
if(line!=='')queue.push(JSON.parse(line))
|
|
33
|
-
}
|
|
34
|
-
notify()
|
|
35
|
-
}
|
|
36
|
-
const stream=carrier.open(endpoint,payload,{
|
|
37
|
-
chunk(text){append(text)},
|
|
38
|
-
end(){
|
|
39
|
-
if(buffer!==''){queue.push(JSON.parse(buffer));buffer=''}
|
|
40
|
-
ended=true
|
|
41
|
-
notify()
|
|
42
|
-
},
|
|
43
|
-
fail(message){failure=new Error(message);ended=true;notify()},
|
|
44
|
-
})
|
|
45
|
-
// 取消必须结束迭代:不叫醒它,等在这一句上的消费方收不到 end 帧,dispose(await iterator.return)
|
|
46
|
-
// 也永不落定——窗口重建会卡在这里。
|
|
47
|
-
const onAbort=()=>{stream.cancel();ended=true;notify()}
|
|
48
|
-
signal.addEventListener('abort',onAbort,{once:true})
|
|
49
|
-
if(signal.aborted)onAbort()
|
|
50
|
-
// 上行(客户端 → 宿主)是逻辑流的输入项:逐条交给主进程,收尾时结束请求体;
|
|
51
|
-
// 没有 uplink 的流(如 $events)立刻结束请求体,宿主侧不用等一个永不来的 end。
|
|
52
|
-
if(uplink===undefined)stream.end()
|
|
53
|
-
else void (async()=>{
|
|
54
|
-
try{
|
|
55
|
-
for await(const item of uplink){
|
|
56
|
-
// 取消后不再上行:宿主那边这条逻辑流已经作废。
|
|
57
|
-
if(signal.aborted)break
|
|
58
|
-
stream.send(item)
|
|
59
|
-
}
|
|
60
|
-
}catch(error){
|
|
61
|
-
console.error('[dsh-desktop] stream uplink failed for',endpoint,error)
|
|
62
|
-
}finally{
|
|
63
|
-
stream.end()
|
|
64
|
-
}
|
|
65
|
-
})()
|
|
66
|
-
try{
|
|
67
|
-
for(;;){
|
|
68
|
-
while(queue.length>0)yield queue.shift()
|
|
69
|
-
if(failure!==undefined)throw failure
|
|
70
|
-
if(ended)return
|
|
71
|
-
await new Promise(resolve=>{wake=resolve})
|
|
72
|
-
}
|
|
73
|
-
}finally{
|
|
74
|
-
signal.removeEventListener('abort',onAbort)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
console.info('[dsh-desktop] transport installed',Object.keys(globalThis.__DSH_TRANSPORT__))`;
|
|
79
|
-
function takeOverDesktopAuthentication(ctx) {
|
|
80
|
-
const connection = ctx.get("connection");
|
|
81
|
-
if (connection === void 0) throw new Error("dsh desktop: the composition did not provide the connection service");
|
|
82
|
-
if (typeof connection.requestRejection !== "function" || typeof connection.authorizeIndex !== "function") throw new Error("dsh desktop: the connection service no longer exposes requestRejection / authorizeIndex; desktop authentication takeover needs an update");
|
|
83
|
-
connection.requestRejection = () => void 0;
|
|
84
|
-
connection.authorizeIndex = () => true;
|
|
85
|
-
}
|
|
86
|
-
function isRecord(value) {
|
|
87
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
88
|
-
}
|
|
89
|
-
async function* readStreamFrames(request) {
|
|
90
|
-
const decoder = new DesktopStreamBodyDecoder();
|
|
91
|
-
for await (const chunk of request) for (const frame of decoder.push(chunk)) yield frame;
|
|
92
|
-
for (const frame of decoder.finish()) yield frame;
|
|
93
|
-
}
|
|
94
|
-
function parseStreamOpening(value, subject) {
|
|
95
|
-
if (!isRecord(value) || typeof value.endpoint !== "string") throw new Error(`dsh desktop: invalid stream request ${subject}`);
|
|
96
|
-
return {
|
|
97
|
-
endpoint: value.endpoint,
|
|
98
|
-
payload: value.payload ?? { args: {} }
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
function reportStreamFailure(subject, detail) {
|
|
102
|
-
console.error(`[dsh-desktop] ${subject}: ${errorText(detail)}`);
|
|
103
|
-
}
|
|
104
|
-
function errorText(value) {
|
|
105
|
-
return value instanceof Error ? value.stack ?? value.message : String(value);
|
|
106
|
-
}
|
|
107
|
-
function streamHandler(ctx) {
|
|
108
|
-
return async (request, response) => {
|
|
109
|
-
if (request.method !== "POST") {
|
|
110
|
-
response.writeHead(405);
|
|
111
|
-
response.end();
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
const gateway = ctx.get("typertGateway");
|
|
115
|
-
if (gateway === void 0) {
|
|
116
|
-
reportStreamFailure("stream failed", "typertGateway service is missing");
|
|
117
|
-
response.writeHead(503);
|
|
118
|
-
response.end("gateway unavailable");
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
const endpointFromQuery = new URL(request.url ?? "/", "http://127.0.0.1").searchParams.get("endpoint");
|
|
122
|
-
const frames = readStreamFrames(request);
|
|
123
|
-
let opening;
|
|
124
|
-
try {
|
|
125
|
-
const first = await frames.next();
|
|
126
|
-
opening = first.done ? parseStreamOpening(endpointFromQuery === null ? void 0 : { endpoint: endpointFromQuery }, String(request.url)) : parseStreamOpening(first.value, String(request.url));
|
|
127
|
-
} catch (error) {
|
|
128
|
-
reportStreamFailure("stream body is not a valid opener", error);
|
|
129
|
-
response.writeHead(400);
|
|
130
|
-
response.end("invalid stream request");
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const endpoint = opening.endpoint;
|
|
134
|
-
const abort = new AbortController();
|
|
135
|
-
const cancel = () => {
|
|
136
|
-
abort.abort();
|
|
137
|
-
};
|
|
138
|
-
request.once("aborted", cancel);
|
|
139
|
-
response.once("close", cancel);
|
|
140
|
-
try {
|
|
141
|
-
console.error(`[dsh-desktop] stream opening ${endpoint}`);
|
|
142
|
-
const values = await gateway.wireStream.open(endpoint, opening.payload, frames, void 0, abort.signal);
|
|
143
|
-
console.error(`[dsh-desktop] stream opened ${endpoint}`);
|
|
144
|
-
response.writeHead(200, {
|
|
145
|
-
"content-type": "application/x-ndjson",
|
|
146
|
-
"cache-control": "no-store"
|
|
147
|
-
});
|
|
148
|
-
for await (const value of values) response.write(`${JSON.stringify(value)}\n`);
|
|
149
|
-
response.end();
|
|
150
|
-
} catch (error) {
|
|
151
|
-
reportStreamFailure(`stream ${endpoint} failed`, error);
|
|
152
|
-
if (response.headersSent) {
|
|
153
|
-
response.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
response.writeHead(502);
|
|
157
|
-
response.end(error instanceof Error ? error.message : String(error));
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
function installDesktopTransport(ctx) {
|
|
162
|
-
ctx.on("webserver/index-inject", (table) => {
|
|
163
|
-
table.push({
|
|
164
|
-
kind: "script",
|
|
165
|
-
placement: "head",
|
|
166
|
-
text: DESKTOP_TRANSPORT_SCRIPT
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
ctx.effect(() => ctx.webServer.register({
|
|
170
|
-
kind: "exact",
|
|
171
|
-
path: DESKTOP_STREAM_PATH,
|
|
172
|
-
handler: streamHandler(ctx)
|
|
173
|
-
}), "dsh-desktop: remote stream route");
|
|
174
|
-
}
|
|
175
|
-
//#endregion
|
|
176
|
-
//#region src/index.ts
|
|
177
|
-
const DESKTOP_PATCH = fileURLToPath(new URL("../config/desktop.cordis.patch.yml", import.meta.url));
|
|
178
|
-
function errorMessage(error) {
|
|
179
|
-
return error instanceof Error ? error.message : String(error);
|
|
180
|
-
}
|
|
181
|
-
const MAX_FATAL_DIAGNOSTIC_CHARS = 65536;
|
|
182
|
-
function fatalEvent(error) {
|
|
183
|
-
return {
|
|
184
|
-
type: "fatal",
|
|
185
|
-
message: errorMessage(error),
|
|
186
|
-
diagnostic: inspect(error, {
|
|
187
|
-
depth: 4,
|
|
188
|
-
maxArrayLength: 50
|
|
189
|
-
}).slice(0, MAX_FATAL_DIAGNOSTIC_CHARS)
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
async function main() {
|
|
193
|
-
const runtimeDir = process.argv[2];
|
|
194
|
-
const projectDir = process.argv[3];
|
|
195
|
-
if (runtimeDir === void 0 || projectDir === void 0 || process.send === void 0) throw new Error("dsh desktop: expected runtime and profile directories, byte pipes, and a Node IPC channel");
|
|
196
|
-
const pnpmEntry = process.argv[4];
|
|
197
|
-
const nodeBin = process.argv[5];
|
|
198
|
-
const requestPipe = createReadStream("", {
|
|
199
|
-
fd: 3,
|
|
200
|
-
autoClose: false
|
|
201
|
-
});
|
|
202
|
-
const responsePipe = createWriteStream("", {
|
|
203
|
-
fd: 4,
|
|
204
|
-
autoClose: false
|
|
205
|
-
});
|
|
206
|
-
let responseTail = Promise.resolve();
|
|
207
|
-
let closing = false;
|
|
208
|
-
responsePipe.on("error", (error) => {
|
|
209
|
-
if (!closing) {
|
|
210
|
-
console.error(error);
|
|
211
|
-
process.exitCode = 1;
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
const writeResponse = (frame) => {
|
|
215
|
-
if (closing || responsePipe.destroyed) return Promise.resolve();
|
|
216
|
-
const write = responseTail.then(async () => {
|
|
217
|
-
if (closing || responsePipe.destroyed) return;
|
|
218
|
-
if (!responsePipe.write(frame)) await once(responsePipe, "drain");
|
|
219
|
-
});
|
|
220
|
-
responseTail = write.catch(() => void 0);
|
|
221
|
-
return write;
|
|
222
|
-
};
|
|
223
|
-
const send = (event) => {
|
|
224
|
-
if (process.send === void 0 || !process.connected) return;
|
|
225
|
-
try {
|
|
226
|
-
process.send(event);
|
|
227
|
-
} catch (error) {
|
|
228
|
-
if (error.code !== "ERR_IPC_CHANNEL_CLOSED") throw error;
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
const installAnchor = join(runtimeDir, "node_modules", "@deepseek-ai", "dsh", "package.json");
|
|
232
|
-
const profile = loadProfileDirectory("dsh", projectDir, installAnchor);
|
|
233
|
-
reportSkippedBundles("dsh", profile);
|
|
234
|
-
const application = runProfile({
|
|
235
|
-
environment: loadLayeredEnv("dsh"),
|
|
236
|
-
profile: "desktop",
|
|
237
|
-
resolvedProfile: {
|
|
238
|
-
profile,
|
|
239
|
-
installAnchor
|
|
240
|
-
},
|
|
241
|
-
patchFiles: [DESKTOP_PATCH],
|
|
242
|
-
args: ["--no-open"],
|
|
243
|
-
...pnpmEntry === void 0 ? {} : { packageManager: {
|
|
244
|
-
command: process.execPath,
|
|
245
|
-
args: ["--expose-internals", pnpmEntry],
|
|
246
|
-
env: {
|
|
247
|
-
ELECTRON_RUN_AS_NODE: "1",
|
|
248
|
-
DSH_DESKTOP_NODE_EXECUTABLE: process.execPath,
|
|
249
|
-
PATH: `${nodeBin ?? ""}${nodeBin === void 0 ? "" : ":"}${process.env.PATH ?? ""}`
|
|
250
|
-
}
|
|
251
|
-
} }
|
|
252
|
-
});
|
|
253
|
-
const { ctx } = await application;
|
|
254
|
-
takeOverDesktopAuthentication(ctx);
|
|
255
|
-
installDesktopTransport(ctx);
|
|
256
|
-
const webServer = ctx.get("webServer");
|
|
257
|
-
const pending = /* @__PURE__ */ new Map();
|
|
258
|
-
const runs = /* @__PURE__ */ new Set();
|
|
259
|
-
const decoder = new DesktopHostRequestDecoder();
|
|
260
|
-
let lastStreamId = 0;
|
|
261
|
-
let stopping;
|
|
262
|
-
const dispatch = async (streamId, request, entry) => {
|
|
263
|
-
try {
|
|
264
|
-
const response = await webServer.dispatch(request);
|
|
265
|
-
await writeResponse(encodeDesktopResponseStart(streamId, {
|
|
266
|
-
status: response.status,
|
|
267
|
-
headers: [...response.headers.entries()],
|
|
268
|
-
hasBody: response.body !== null
|
|
269
|
-
}));
|
|
270
|
-
if (response.body !== null) for await (const chunk of response.body) {
|
|
271
|
-
const bytes = Buffer.from(chunk);
|
|
272
|
-
for (let offset = 0; offset < bytes.byteLength; offset += DESKTOP_PIPE_CHUNK_BYTES) {
|
|
273
|
-
if (!pending.has(streamId)) return;
|
|
274
|
-
await writeResponse(encodeDesktopResponseData(streamId, bytes.subarray(offset, offset + DESKTOP_PIPE_CHUNK_BYTES)));
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
await writeResponse(encodeDesktopResponseEnd(streamId));
|
|
278
|
-
} catch (error) {
|
|
279
|
-
if (!entry.abort.signal.aborted) await writeResponse(encodeDesktopResponseError(streamId, errorMessage(error))).catch(() => {});
|
|
280
|
-
} finally {
|
|
281
|
-
pending.delete(streamId);
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
const handleFrame = (frame) => {
|
|
285
|
-
if (frame.type === "start") {
|
|
286
|
-
if (frame.streamId <= lastStreamId) throw new Error(`dsh desktop: request stream ${String(frame.streamId)} is not in order`);
|
|
287
|
-
lastStreamId = frame.streamId;
|
|
288
|
-
const entry = { abort: new AbortController() };
|
|
289
|
-
const streamId = frame.streamId;
|
|
290
|
-
const body = frame.hasBody ? new ReadableStream({
|
|
291
|
-
start: (controller) => {
|
|
292
|
-
entry.body = controller;
|
|
293
|
-
},
|
|
294
|
-
cancel: () => {
|
|
295
|
-
entry.abort.abort();
|
|
296
|
-
}
|
|
297
|
-
}) : void 0;
|
|
298
|
-
pending.set(streamId, entry);
|
|
299
|
-
const request = new Request(frame.url, {
|
|
300
|
-
method: frame.method,
|
|
301
|
-
headers: frame.headers,
|
|
302
|
-
signal: entry.abort.signal,
|
|
303
|
-
...body === void 0 ? {} : {
|
|
304
|
-
body,
|
|
305
|
-
duplex: "half"
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
|
-
const run = dispatch(streamId, request, entry).catch((error) => {
|
|
309
|
-
ctx.logger.warn(error instanceof Error ? error : new Error(errorMessage(error)));
|
|
310
|
-
});
|
|
311
|
-
runs.add(run);
|
|
312
|
-
run.finally(() => runs.delete(run));
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
const entry = pending.get(frame.streamId);
|
|
316
|
-
if (entry === void 0) return;
|
|
317
|
-
switch (frame.type) {
|
|
318
|
-
case "data":
|
|
319
|
-
entry.body?.enqueue(frame.data);
|
|
320
|
-
return;
|
|
321
|
-
case "end":
|
|
322
|
-
entry.body?.close();
|
|
323
|
-
return;
|
|
324
|
-
case "cancel":
|
|
325
|
-
entry.abort.abort();
|
|
326
|
-
return;
|
|
327
|
-
default: return;
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
const stop = (exitCode = 0) => {
|
|
331
|
-
stopping ??= (async () => {
|
|
332
|
-
closing = true;
|
|
333
|
-
requestPipe.pause();
|
|
334
|
-
requestPipe.destroy();
|
|
335
|
-
for (const entry of pending.values()) entry.abort.abort();
|
|
336
|
-
pending.clear();
|
|
337
|
-
await (await application.catch(() => void 0))?.shutdown.shutdown(exitCode);
|
|
338
|
-
await Promise.allSettled(runs);
|
|
339
|
-
await responseTail.catch(() => void 0);
|
|
340
|
-
if (!responsePipe.destroyed) responsePipe.destroy();
|
|
341
|
-
if (process.connected) process.disconnect?.();
|
|
342
|
-
process.exitCode = exitCode;
|
|
343
|
-
})();
|
|
344
|
-
return stopping;
|
|
345
|
-
};
|
|
346
|
-
requestPipe.on("data", (chunk) => {
|
|
347
|
-
let frames;
|
|
348
|
-
try {
|
|
349
|
-
frames = decoder.push(chunk);
|
|
350
|
-
for (const frame of frames) handleFrame(frame);
|
|
351
|
-
} catch (error) {
|
|
352
|
-
send(fatalEvent(error));
|
|
353
|
-
stop(1);
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
requestPipe.once("error", (error) => {
|
|
357
|
-
send(fatalEvent(error));
|
|
358
|
-
stop(1);
|
|
359
|
-
});
|
|
360
|
-
requestPipe.once("end", () => {
|
|
361
|
-
try {
|
|
362
|
-
decoder.finish();
|
|
363
|
-
} catch (error) {
|
|
364
|
-
send(fatalEvent(error));
|
|
365
|
-
stop(1);
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
process.on("message", (message) => {
|
|
369
|
-
if (isDesktopHostCommand(message)) stop();
|
|
370
|
-
});
|
|
371
|
-
process.once("disconnect", () => {
|
|
372
|
-
stop();
|
|
373
|
-
});
|
|
374
|
-
const gateway = ctx.get("typertGateway");
|
|
375
|
-
console.error(`[dsh-desktop] transport ready: webServer=${webServer.constructor.name} gateway=${gateway === void 0 ? "missing" : "present"} streamRoute=${DESKTOP_STREAM_PATH}`);
|
|
376
|
-
send({
|
|
377
|
-
type: "ready",
|
|
378
|
-
protocolVersion: 1
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
if (import.meta.main) main().catch((error) => {
|
|
382
|
-
if (process.send !== void 0 && process.connected) process.send(fatalEvent(error), (sendError) => {
|
|
383
|
-
if (sendError !== null) console.error(sendError);
|
|
384
|
-
});
|
|
385
|
-
console.error(error);
|
|
386
|
-
process.exitCode = 1;
|
|
387
|
-
if (process.connected) process.disconnect?.();
|
|
388
|
-
});
|
|
389
|
-
//#endregion
|
|
390
|
-
export {};
|
package/lib/webserver.js
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "node:events";
|
|
2
|
-
import { Readable } from "node:stream";
|
|
3
|
-
import { Service } from "@deepseek-ai/cordis";
|
|
4
|
-
import z from "@deepseek-ai/schemastery";
|
|
5
|
-
import { renderIndexInjections } from "@deepseek-ai/dsh-host-webserver";
|
|
6
|
-
//#region src/webserver.ts
|
|
7
|
-
function deferred() {
|
|
8
|
-
let resolve;
|
|
9
|
-
return {
|
|
10
|
-
promise: new Promise((settle) => {
|
|
11
|
-
resolve = settle;
|
|
12
|
-
}),
|
|
13
|
-
resolve
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
var SyntheticResponse = class extends EventEmitter {
|
|
17
|
-
statusCode = 200;
|
|
18
|
-
statusMessage = "";
|
|
19
|
-
headersSent = false;
|
|
20
|
-
writableEnded = false;
|
|
21
|
-
destroyed = false;
|
|
22
|
-
socket = void 0;
|
|
23
|
-
body;
|
|
24
|
-
started;
|
|
25
|
-
done;
|
|
26
|
-
startSettle;
|
|
27
|
-
doneSettle;
|
|
28
|
-
headers = new Headers();
|
|
29
|
-
controller;
|
|
30
|
-
startedValue;
|
|
31
|
-
constructor() {
|
|
32
|
-
super();
|
|
33
|
-
this.startSettle = deferred();
|
|
34
|
-
this.doneSettle = deferred();
|
|
35
|
-
this.started = this.startSettle.promise;
|
|
36
|
-
this.done = this.doneSettle.promise;
|
|
37
|
-
this.body = new ReadableStream({
|
|
38
|
-
start: (controller) => {
|
|
39
|
-
this.controller = controller;
|
|
40
|
-
},
|
|
41
|
-
cancel: () => {
|
|
42
|
-
this.destroy();
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
setHeader(name, value) {
|
|
47
|
-
this.headers.set(name, Array.isArray(value) ? value.join(", ") : String(value));
|
|
48
|
-
}
|
|
49
|
-
getHeader(name) {
|
|
50
|
-
return this.headers.get(name);
|
|
51
|
-
}
|
|
52
|
-
getHeaders() {
|
|
53
|
-
return new Headers(this.headers);
|
|
54
|
-
}
|
|
55
|
-
removeHeader(name) {
|
|
56
|
-
this.headers.delete(name);
|
|
57
|
-
}
|
|
58
|
-
getHeaderNames() {
|
|
59
|
-
return [...this.headers.keys()];
|
|
60
|
-
}
|
|
61
|
-
writeHead(status, headers) {
|
|
62
|
-
this.statusCode = status;
|
|
63
|
-
if (headers !== void 0) for (const [name, value] of Object.entries(headers)) this.setHeader(name, value);
|
|
64
|
-
this.settleStart();
|
|
65
|
-
return this;
|
|
66
|
-
}
|
|
67
|
-
write(chunk) {
|
|
68
|
-
if (this.writableEnded) throw new Error("dsh desktop: response already ended");
|
|
69
|
-
this.settleStart();
|
|
70
|
-
if (!this.destroyed) this.controller.enqueue(toBytes(chunk));
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
end(chunk) {
|
|
74
|
-
if (this.writableEnded) return this;
|
|
75
|
-
this.settleStart();
|
|
76
|
-
if (chunk !== void 0 && !this.destroyed) this.controller.enqueue(toBytes(chunk));
|
|
77
|
-
this.writableEnded = true;
|
|
78
|
-
if (!this.destroyed) this.controller.close();
|
|
79
|
-
this.doneSettle.resolve();
|
|
80
|
-
this.emit("finish");
|
|
81
|
-
this.emit("close");
|
|
82
|
-
return this;
|
|
83
|
-
}
|
|
84
|
-
destroy(error) {
|
|
85
|
-
if (this.destroyed) return this;
|
|
86
|
-
this.destroyed = true;
|
|
87
|
-
if (!this.writableEnded) {
|
|
88
|
-
this.settleStart(500);
|
|
89
|
-
this.controller.error(error ?? /* @__PURE__ */ new Error("dsh desktop: response destroyed"));
|
|
90
|
-
this.doneSettle.resolve();
|
|
91
|
-
}
|
|
92
|
-
this.emit("close");
|
|
93
|
-
return this;
|
|
94
|
-
}
|
|
95
|
-
fail(error) {
|
|
96
|
-
if (this.destroyed) return;
|
|
97
|
-
if (!this.headersSent) {
|
|
98
|
-
this.statusCode = 500;
|
|
99
|
-
this.settleStart();
|
|
100
|
-
this.writableEnded = true;
|
|
101
|
-
this.controller.enqueue(new TextEncoder().encode(errorMessage(error)));
|
|
102
|
-
this.controller.close();
|
|
103
|
-
this.doneSettle.resolve();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
this.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
107
|
-
}
|
|
108
|
-
settleStart(forcedStatus) {
|
|
109
|
-
if (this.startedValue !== void 0) return;
|
|
110
|
-
this.headersSent = true;
|
|
111
|
-
if (forcedStatus !== void 0) this.statusCode = forcedStatus;
|
|
112
|
-
this.startedValue = {
|
|
113
|
-
status: this.statusCode,
|
|
114
|
-
headers: new Headers(this.headers)
|
|
115
|
-
};
|
|
116
|
-
this.startSettle.resolve(this.startedValue);
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
function toBytes(chunk) {
|
|
120
|
-
return typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
|
|
121
|
-
}
|
|
122
|
-
function errorMessage(error) {
|
|
123
|
-
return error instanceof Error ? error.message : String(error);
|
|
124
|
-
}
|
|
125
|
-
function createRequest(request, url) {
|
|
126
|
-
const stream = Readable.from(request.body === null ? [] : consume(request.body));
|
|
127
|
-
const headers = {};
|
|
128
|
-
for (const [name, value] of request.headers) headers[name.toLowerCase()] = value;
|
|
129
|
-
return Object.assign(stream, {
|
|
130
|
-
url: `${url.pathname}${url.search}`,
|
|
131
|
-
method: request.method,
|
|
132
|
-
headers,
|
|
133
|
-
httpVersion: "1.1",
|
|
134
|
-
httpVersionMajor: 1,
|
|
135
|
-
httpVersionMinor: 1,
|
|
136
|
-
complete: false,
|
|
137
|
-
aborted: false
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
async function* consume(body) {
|
|
141
|
-
const reader = body.getReader();
|
|
142
|
-
try {
|
|
143
|
-
for (;;) {
|
|
144
|
-
const next = await reader.read();
|
|
145
|
-
if (next.done) return;
|
|
146
|
-
yield next.value;
|
|
147
|
-
}
|
|
148
|
-
} finally {
|
|
149
|
-
reader.releaseLock();
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
var PortlessWebServer = class extends Service {
|
|
153
|
-
config;
|
|
154
|
-
static Config = z.object({
|
|
155
|
-
host: z.union([z.const("127.0.0.1"), z.const("0.0.0.0")]).default("127.0.0.1"),
|
|
156
|
-
port: z.natural().max(65535).default(0)
|
|
157
|
-
});
|
|
158
|
-
exact = /* @__PURE__ */ new Map();
|
|
159
|
-
prefixes = /* @__PURE__ */ new Map();
|
|
160
|
-
upgrades = /* @__PURE__ */ new Map();
|
|
161
|
-
indexTaps = [];
|
|
162
|
-
fallback;
|
|
163
|
-
constructor(ctx, config) {
|
|
164
|
-
super(ctx, "webServer");
|
|
165
|
-
this.config = config;
|
|
166
|
-
}
|
|
167
|
-
get port() {
|
|
168
|
-
return this.config.port ?? 0;
|
|
169
|
-
}
|
|
170
|
-
get host() {
|
|
171
|
-
return this.config.host ?? "127.0.0.1";
|
|
172
|
-
}
|
|
173
|
-
register(route) {
|
|
174
|
-
const table = route.kind === "exact" ? this.exact : this.prefixes;
|
|
175
|
-
if (table.has(route.path)) throw new Error(`webserver: duplicate ${route.kind} route "${route.path}"`);
|
|
176
|
-
table.set(route.path, route);
|
|
177
|
-
return () => {
|
|
178
|
-
table.delete(route.path);
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
registerUpgrade(route) {
|
|
182
|
-
if (this.upgrades.has(route.path)) throw new Error(`webserver: duplicate upgrade route "${route.path}"`);
|
|
183
|
-
this.upgrades.set(route.path, route);
|
|
184
|
-
return () => {
|
|
185
|
-
this.upgrades.delete(route.path);
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
registerFallback(handler) {
|
|
189
|
-
if (this.fallback !== void 0) throw new Error("webserver: fallback already registered");
|
|
190
|
-
this.fallback = handler;
|
|
191
|
-
return () => {
|
|
192
|
-
this.fallback = void 0;
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
tapIndex(transform) {
|
|
196
|
-
this.indexTaps.push(transform);
|
|
197
|
-
return () => {
|
|
198
|
-
const at = this.indexTaps.indexOf(transform);
|
|
199
|
-
if (at !== -1) this.indexTaps.splice(at, 1);
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
applyIndexTaps(html) {
|
|
203
|
-
let out = html;
|
|
204
|
-
for (const transform of this.indexTaps) out = transform(out);
|
|
205
|
-
return out;
|
|
206
|
-
}
|
|
207
|
-
collectIndexInjections() {
|
|
208
|
-
const table = [];
|
|
209
|
-
this.ctx.emit("webserver/index-inject", table);
|
|
210
|
-
return table;
|
|
211
|
-
}
|
|
212
|
-
renderIndex(html) {
|
|
213
|
-
return this.applyIndexTaps(renderIndexInjections(html, this.collectIndexInjections()));
|
|
214
|
-
}
|
|
215
|
-
async dispatch(request) {
|
|
216
|
-
const url = new URL(request.url);
|
|
217
|
-
let pathname;
|
|
218
|
-
try {
|
|
219
|
-
pathname = decodeURIComponent(url.pathname);
|
|
220
|
-
} catch {
|
|
221
|
-
return new Response(null, { status: 400 });
|
|
222
|
-
}
|
|
223
|
-
const handler = this.match(pathname)?.handler ?? this.fallback;
|
|
224
|
-
if (process.env.DSH_DESKTOP_DEBUG === "1") console.error(`[dsh-desktop] dispatch ${request.method} ${pathname} → ${handler === void 0 ? "404" : "handler"}`);
|
|
225
|
-
if (handler === void 0) return new Response("not found", { status: 404 });
|
|
226
|
-
const response = new SyntheticResponse();
|
|
227
|
-
const req = createRequest(request, url);
|
|
228
|
-
Promise.resolve().then(() => handler(req, response)).catch((error) => {
|
|
229
|
-
this.ctx.logger.warn(error instanceof Error ? error : new Error(errorMessage(error)));
|
|
230
|
-
response.fail(error);
|
|
231
|
-
});
|
|
232
|
-
const started = await response.started;
|
|
233
|
-
return new Response(response.body, {
|
|
234
|
-
status: started.status,
|
|
235
|
-
headers: started.headers
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
match(pathname) {
|
|
239
|
-
const exact = this.exact.get(pathname);
|
|
240
|
-
if (exact !== void 0) return exact;
|
|
241
|
-
let best;
|
|
242
|
-
for (const [prefix, route] of this.prefixes) {
|
|
243
|
-
if (pathname !== prefix && !pathname.startsWith(`${prefix}/`)) continue;
|
|
244
|
-
if (best === void 0 || prefix.length > best.path.length) best = route;
|
|
245
|
-
}
|
|
246
|
-
return best;
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
//#endregion
|
|
250
|
-
export { PortlessWebServer, PortlessWebServer as default };
|