@morlay/dsh-desktop-host 0.2.1 → 0.2.3
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 +6 -52
- package/lib/index.js +4 -63
- package/lib/webserver.js +0 -14
- package/lib/wire.js +0 -38
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -1,61 +1,15 @@
|
|
|
1
1
|
# @morlay/dsh-desktop-host
|
|
2
2
|
|
|
3
|
-
桌面部署里那个 **host 进程**:按 `desktop` profile 把 Web 应用启起来,然后**在进程内接管 `webServer
|
|
4
|
-
不监听任何端口,Electron 壳把该应用自己的协议(`<app name>://app/*`)的请求经字节管道(FD 3/4)喂进来。
|
|
3
|
+
桌面部署里那个 **host 进程**:按 `desktop` profile 把 Web 应用启起来,然后**在进程内接管 `webServer`**——不监听任何端口,Electron 壳把该应用自己的协议(`<app name>://app/*`)的请求经字节管道(FD 3/4)喂进来。
|
|
5
4
|
|
|
6
|
-
上游 `apps/desktop-host`(`@deepseek-ai/dsh-desktop-host`)是 `private: true`
|
|
7
|
-
是它的**变体包**:argv / IPC 契约、`lib/index.js` 入口路径照旧,两处本地差别——Office 组合不再挂
|
|
8
|
-
`@deepseek-ai/dsh-skill-office`(见 [`src/office.ts`](./src/office.ts)),载波从「监听端口 + 壳认证反向代理」
|
|
9
|
-
换成「宿主内 webServer 替身 + 管道」(见 [`src/webserver.ts`](./src/webserver.ts) 与桌面层的
|
|
10
|
-
[设计 桌面无端口传输与窗口对齐](../dsh-desktopify/.agents/designs/20260920-桌面无端口传输与窗口对齐.md))。
|
|
5
|
+
上游 `apps/desktop-host`(`@deepseek-ai/dsh-desktop-host`)是 `private: true` 的应用,外部装不到,所以这里是它的**变体包**:argv / IPC 契约、`lib/index.js` 入口路径照旧。
|
|
11
6
|
|
|
12
7
|
## 用法
|
|
13
8
|
|
|
14
9
|
被 [`@morlay/dsh-desktopify`](../dsh-desktopify/README.md) 使用,不单独跑:
|
|
15
10
|
|
|
16
|
-
- `dev` / `bundle` 把本包落位到部署的 `<runtime>/node_modules/@morlay/dsh-desktop-host
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
`config/desktop.cordis.patch.yml`(入口按 `../config/desktop.cordis.patch.yml` 作为 `patchFiles` 读),
|
|
20
|
-
缺任一样 host 都起不来;
|
|
21
|
-
- 壳按 `<runtime>/node_modules/@morlay/dsh-desktop-host/lib/index.js` 启动它(stdio 五元组:
|
|
22
|
-
FD 3/4 是管道,FD 5 是 Node IPC),argv 为
|
|
23
|
-
`[runtimeDir, projectDir, primaryRuntime, pnpmEntry, nodeBin]`(上游 0.1.7 删掉 `runProfile` 的 `resolutionMode` 后少一格);
|
|
24
|
-
- IPC:`ready { protocolVersion }` / `fatal { message, diagnostic }`(`diagnostic` 是完整 `util.inspect` 的错误,
|
|
25
|
-
与上游 0.1.7-alpha.2 同形;壳的 dialog 用 `message`,完整诊断走 stderr),另外收 `shutdown`。
|
|
11
|
+
- `dev` / `bundle` 把本包落位到部署的 `<runtime>/node_modules/@morlay/dsh-desktop-host`(`dev` 整份复制源码树,`bundle` 按 manifest `files` 复制),四样必须都在——`lib/index.js`(启动入口)、`lib/webserver.js`(桌面 patch 行按 `../lib/webserver.js` 加载)、`lib/wire.js`、`config/desktop.cordis.patch.yml`(入口按它作 `patchFiles` 读);
|
|
12
|
+
- 壳按 `lib/index.js` 启动它(stdio 五元组:FD 3/4 是管道,FD 5 是 Node IPC),argv 为 `[entry, runtimeDir, projectDir, pnpmEntry?, nodeBin?]`;
|
|
13
|
+
- IPC:`ready { protocolVersion }` / `fatal { message, diagnostic }`,另外收 `shutdown`。
|
|
26
14
|
|
|
27
|
-
|
|
28
|
-
(管道分帧,壳构建时内联,部署里不需要单独解析它)。
|
|
29
|
-
|
|
30
|
-
## 运行时做的事
|
|
31
|
-
|
|
32
|
-
1. `runProfile` 装配 `desktop` profile(保住 profileContext / proxy / fail-loud / appReady / shutdown 语义);
|
|
33
|
-
2. 桌面 patch 禁用上游 `webserver` 行,插入本包的 `webServer` 替身——同一份路由面
|
|
34
|
-
(`register` / `registerFallback` / `registerUpgrade` / `tapIndex` / `renderIndex` / `collectIndexInjections` /
|
|
35
|
-
`port` / `host`),请求由 `dispatch(Request)` 从管道喂进来;
|
|
36
|
-
3. 接管浏览器认证:页面由壳独占、没有网络入口,`connection` 的 `requestRejection` 与 `authorizeIndex`
|
|
37
|
-
被改写为放行(理由与代价见[债务 桌面宿主覆写 connection 认证方法](../dsh-desktopify/.agents/debts/20260920-桌面宿主覆写connection认证方法.md));
|
|
38
|
-
4. 注入客户端 transport(`__DSH_TRANSPORT__ = { ownsHost, openStream }`)并注册 `/.dsh/remote-stream`
|
|
39
|
-
(双向 NDJSON:请求体首行定 endpoint/payload、后续行是上行项,响应体是下行帧 →
|
|
40
|
-
`typertGateway.wireStream.open`);
|
|
41
|
-
5. 桌面 patch 还按 id 停掉整条**账号/登录面**(`deepseek-account`、`account-controller`、
|
|
42
|
-
`llm-deepseek-account`、`ui-settings-account`):登录要的回环地址回调在无端口下没有入口,
|
|
43
|
-
装着只会撞死路——理由与代价见 [ADR 桌面档不含登录与账号面](./.agents/adrs/20260928-桌面档不含登录与账号面.md)。
|
|
44
|
-
模型路径不受影响(`credentials` + `llm-pi-ai` 的 `ollama` 路由),探针
|
|
45
|
-
[`tool/verify-desktop-account-plane.mts`](./tool/verify-desktop-account-plane.mts) 判这两件事。
|
|
46
|
-
|
|
47
|
-
## 依赖边界
|
|
48
|
-
|
|
49
|
-
`@deepseek-ai/*` 全部由**部署自己的** `node_modules` 提供(与上游同边界):
|
|
50
|
-
|
|
51
|
-
- `dependencies`:本包入口与替身真正 import 的 `@deepseek-ai/dsh`(含 `./profile-boot` 子路径)、
|
|
52
|
-
`@deepseek-ai/dsh-app-boot`、`@deepseek-ai/dsh-home-paths`、`@deepseek-ai/dsh-host-webserver`
|
|
53
|
-
(`renderIndexInjections`)、`@deepseek-ai/schemastery`(替身的 Config schema);
|
|
54
|
-
- `peerDependencies`:`@deepseek-ai/cordis`;
|
|
55
|
-
- 类型面 `@deepseek-ai/dsh-client-connection` / `@deepseek-ai/dsh-api-gateway` 只在 devDependencies
|
|
56
|
-
(空 import 带 Context merge,构建后即被擦除);
|
|
57
|
-
- vendor 里的 `workspace-dependencies` 模块在构建期内联,运行期不依赖 vendor 路径。
|
|
58
|
-
|
|
59
|
-
清单只声明**这份代码真正用到**的包:上游那份 app manifest 里的组合依赖
|
|
60
|
-
(`@deepseek-ai/dsh-skill-office`、`dsh-agent`、`dsh-jobs`、`dsh-tools` …)属于部署的官方闭包,
|
|
61
|
-
由 `@morlay/dsh-desktopify` 的部署步骤按 profile 提供,不该由本包声明。
|
|
15
|
+
运行时做的事(profile 装配、`webServer` 替身、认证接管、客户端 transport 与账号面)、出口面与依赖边界见[设计 桌面host的运行时面与依赖边界](./.agents/designs/20260929-桌面host的运行时面与依赖边界.md);载波形态的取舍见[设计 桌面无端口传输与窗口对齐](../dsh-desktopify/.agents/designs/20260920-桌面无端口传输与窗口对齐.md)。
|
package/lib/index.js
CHANGED
|
@@ -6,38 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { inspect } from "node:util";
|
|
7
7
|
import { loadLayeredEnv, loadProfileDirectory, reportSkippedBundles } from "@deepseek-ai/dsh-app-boot";
|
|
8
8
|
import { runProfile } from "@deepseek-ai/dsh/profile-boot";
|
|
9
|
-
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
10
|
-
import * as workspaceDependencies from "@deepseek-ai/dsh-tool-workspace-dependencies";
|
|
11
|
-
//#region \0rolldown/runtime.js
|
|
12
|
-
var __defProp = Object.defineProperty;
|
|
13
|
-
var __exportAll = (all, no_symbols) => {
|
|
14
|
-
let target = {};
|
|
15
|
-
for (var name in all) __defProp(target, name, {
|
|
16
|
-
get: all[name],
|
|
17
|
-
enumerable: true
|
|
18
|
-
});
|
|
19
|
-
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
20
|
-
return target;
|
|
21
|
-
};
|
|
22
|
-
//#endregion
|
|
23
|
-
//#region src/office.ts
|
|
24
|
-
var office_exports = /* @__PURE__ */ __exportAll({
|
|
25
|
-
apply: () => apply,
|
|
26
|
-
name: () => name
|
|
27
|
-
});
|
|
28
|
-
/** Loader identity for the application-owned workspace dependency composition. */
|
|
29
|
-
const name = "desktop-office";
|
|
30
|
-
/**
|
|
31
|
-
* Enable the bundled authoring dependencies in the Desktop profile.
|
|
32
|
-
* @param ctx - Profile scope; child plugins declare their own service requirements.
|
|
33
|
-
* @param config - Bundled payload source and Harness-home installation root.
|
|
34
|
-
*/
|
|
35
|
-
async function apply(ctx, config) {
|
|
36
|
-
await ctx.plugin(workspaceDependencies, config);
|
|
37
|
-
}
|
|
38
|
-
//#endregion
|
|
39
9
|
//#region src/transport.ts
|
|
40
|
-
/** 注入 index 的 transport 行:声明页面拥有 Host,并给出 Gateway 流载体(下行流 + 上行项)。 */
|
|
41
10
|
const DESKTOP_TRANSPORT_SCRIPT = `globalThis.__DSH_TRANSPORT__={
|
|
42
11
|
_desktop:true,
|
|
43
12
|
ownsHost:true,
|
|
@@ -73,9 +42,8 @@ const DESKTOP_TRANSPORT_SCRIPT = `globalThis.__DSH_TRANSPORT__={
|
|
|
73
42
|
},
|
|
74
43
|
fail(message){failure=new Error(message);ended=true;notify()},
|
|
75
44
|
})
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
// 撤回 / 重试后的窗口重建正是卡在这里,页面既收不到新窗口也没有报错。
|
|
45
|
+
// 取消必须结束迭代:不叫醒它,等在这一句上的消费方收不到 end 帧,dispose(await iterator.return)
|
|
46
|
+
// 也永不落定——窗口重建会卡在这里。
|
|
79
47
|
const onAbort=()=>{stream.cancel();ended=true;notify()}
|
|
80
48
|
signal.addEventListener('abort',onAbort,{once:true})
|
|
81
49
|
if(signal.aborted)onAbort()
|
|
@@ -108,12 +76,6 @@ const DESKTOP_TRANSPORT_SCRIPT = `globalThis.__DSH_TRANSPORT__={
|
|
|
108
76
|
}
|
|
109
77
|
}
|
|
110
78
|
console.info('[dsh-desktop] transport installed',Object.keys(globalThis.__DSH_TRANSPORT__))`;
|
|
111
|
-
/**
|
|
112
|
-
* 桌面形态下放行 `connection` 的两处浏览器认证:`requestRejection`(`/api` 路由与
|
|
113
|
-
* 各插件 handler 的自查)与 `authorizeIndex`(`frontend-static` 的 index 渲染)。
|
|
114
|
-
*
|
|
115
|
-
* 页面由壳独占、没有网络入口,认证没有对象;这里直接改写服务实例的方法。
|
|
116
|
-
*/
|
|
117
79
|
function takeOverDesktopAuthentication(ctx) {
|
|
118
80
|
const connection = ctx.get("connection");
|
|
119
81
|
if (connection === void 0) throw new Error("dsh desktop: the composition did not provide the connection service");
|
|
@@ -124,13 +86,11 @@ function takeOverDesktopAuthentication(ctx) {
|
|
|
124
86
|
function isRecord(value) {
|
|
125
87
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
126
88
|
}
|
|
127
|
-
/** 桌面流请求体:首行是 open(endpoint / payload),后续每行是一道上行项。 */
|
|
128
89
|
async function* readStreamFrames(request) {
|
|
129
90
|
const decoder = new DesktopStreamBodyDecoder();
|
|
130
91
|
for await (const chunk of request) for (const frame of decoder.push(chunk)) yield frame;
|
|
131
92
|
for (const frame of decoder.finish()) yield frame;
|
|
132
93
|
}
|
|
133
|
-
/** 校验首行:它决定这条流开在哪个 endpoint 上。 */
|
|
134
94
|
function parseStreamOpening(value, subject) {
|
|
135
95
|
if (!isRecord(value) || typeof value.endpoint !== "string") throw new Error(`dsh desktop: invalid stream request ${subject}`);
|
|
136
96
|
return {
|
|
@@ -198,7 +158,6 @@ function streamHandler(ctx) {
|
|
|
198
158
|
}
|
|
199
159
|
};
|
|
200
160
|
}
|
|
201
|
-
/** 注入 transport 行并注册 Gateway 流路由;随插件 fiber 一起撤销。 */
|
|
202
161
|
function installDesktopTransport(ctx) {
|
|
203
162
|
ctx.on("webserver/index-inject", (table) => {
|
|
204
163
|
table.push({
|
|
@@ -215,24 +174,11 @@ function installDesktopTransport(ctx) {
|
|
|
215
174
|
}
|
|
216
175
|
//#endregion
|
|
217
176
|
//#region src/index.ts
|
|
218
|
-
/**
|
|
219
|
-
* 桌面部署里的 host 进程入口:按 `desktop` profile 装配 Web 应用,并把请求经字节管道
|
|
220
|
-
* (FD 3/4)交给宿主内的无端口 `webServer`。
|
|
221
|
-
*
|
|
222
|
-
* argv:`[runtimeDir, projectDir, primaryRuntime, pnpmEntry?, nodeBin?]`;
|
|
223
|
-
* IPC:`ready` / `fatal`,另收 `shutdown`。
|
|
224
|
-
*/
|
|
225
177
|
const DESKTOP_PATCH = fileURLToPath(new URL("../config/desktop.cordis.patch.yml", import.meta.url));
|
|
226
178
|
function errorMessage(error) {
|
|
227
179
|
return error instanceof Error ? error.message : String(error);
|
|
228
180
|
}
|
|
229
|
-
/** 诊断上限:壳把它写进日志,再长也没有额外价值。 */
|
|
230
181
|
const MAX_FATAL_DIAGNOSTIC_CHARS = 65536;
|
|
231
|
-
/**
|
|
232
|
-
* fatal 事件:`message` 给壳的 dialog 用,`diagnostic` 是完整 `inspect`(`code` / `syscall` /
|
|
233
|
-
* `path` / `cause` 链,stack 行里没有)——stderr 字节与这条 IPC 谁先到不确定,壳报告的是它
|
|
234
|
-
* 先看到的那一条,所以诊断得跟 IPC 一起走。
|
|
235
|
-
*/
|
|
236
182
|
function fatalEvent(error) {
|
|
237
183
|
return {
|
|
238
184
|
type: "fatal",
|
|
@@ -247,9 +193,8 @@ async function main() {
|
|
|
247
193
|
const runtimeDir = process.argv[2];
|
|
248
194
|
const projectDir = process.argv[3];
|
|
249
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");
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
const nodeBin = process.argv[6];
|
|
196
|
+
const pnpmEntry = process.argv[4];
|
|
197
|
+
const nodeBin = process.argv[5];
|
|
253
198
|
const requestPipe = createReadStream("", {
|
|
254
199
|
fd: 3,
|
|
255
200
|
autoClose: false
|
|
@@ -308,10 +253,6 @@ async function main() {
|
|
|
308
253
|
const { ctx } = await application;
|
|
309
254
|
takeOverDesktopAuthentication(ctx);
|
|
310
255
|
installDesktopTransport(ctx);
|
|
311
|
-
await ctx.plugin(office_exports, {
|
|
312
|
-
source: primaryRuntime,
|
|
313
|
-
root: join(resolveDshHome(), "dsh-runtimes", "dsh-primary-runtime")
|
|
314
|
-
});
|
|
315
256
|
const webServer = ctx.get("webServer");
|
|
316
257
|
const pending = /* @__PURE__ */ new Map();
|
|
317
258
|
const runs = /* @__PURE__ */ new Set();
|
package/lib/webserver.js
CHANGED
|
@@ -4,14 +4,6 @@ import { Service } from "@deepseek-ai/cordis";
|
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { renderIndexInjections } from "@deepseek-ai/dsh-host-webserver";
|
|
6
6
|
//#region src/webserver.ts
|
|
7
|
-
/**
|
|
8
|
-
* 桌面形态的 `webServer` 服务:与上游同名的路由载体,但不监听任何端口。
|
|
9
|
-
*
|
|
10
|
-
* 上游 `@deepseek-ai/dsh-host-webserver` 在激活时 `listen`。桌面宿主自己拥有页面,
|
|
11
|
-
* 没有网络入口,于是这里保留同一份服务面(register / registerFallback / registerUpgrade /
|
|
12
|
-
* tapIndex / renderIndex / collectIndexInjections / port / host),把请求由 `dispatch`
|
|
13
|
-
* 从字节管道直接喂进来。
|
|
14
|
-
*/
|
|
15
7
|
function deferred() {
|
|
16
8
|
let resolve;
|
|
17
9
|
return {
|
|
@@ -21,14 +13,12 @@ function deferred() {
|
|
|
21
13
|
resolve
|
|
22
14
|
};
|
|
23
15
|
}
|
|
24
|
-
/** 由 handler 写出的 node 风格响应:写出头部即对外可见,`end` 收尾响应体流。 */
|
|
25
16
|
var SyntheticResponse = class extends EventEmitter {
|
|
26
17
|
statusCode = 200;
|
|
27
18
|
statusMessage = "";
|
|
28
19
|
headersSent = false;
|
|
29
20
|
writableEnded = false;
|
|
30
21
|
destroyed = false;
|
|
31
|
-
/** 上游中间件按 `res.socket === undefined` 识别「没有真实连接」。 */
|
|
32
22
|
socket = void 0;
|
|
33
23
|
body;
|
|
34
24
|
started;
|
|
@@ -102,7 +92,6 @@ var SyntheticResponse = class extends EventEmitter {
|
|
|
102
92
|
this.emit("close");
|
|
103
93
|
return this;
|
|
104
94
|
}
|
|
105
|
-
/** handler 抛错:未写头时以 500 收场,已写头则中断响应体。 */
|
|
106
95
|
fail(error) {
|
|
107
96
|
if (this.destroyed) return;
|
|
108
97
|
if (!this.headersSent) {
|
|
@@ -133,7 +122,6 @@ function toBytes(chunk) {
|
|
|
133
122
|
function errorMessage(error) {
|
|
134
123
|
return error instanceof Error ? error.message : String(error);
|
|
135
124
|
}
|
|
136
|
-
/** 把一条 Fetch 请求合成 node 风格的 req/res,供注册在这些表上的 handler 使用。 */
|
|
137
125
|
function createRequest(request, url) {
|
|
138
126
|
const stream = Readable.from(request.body === null ? [] : consume(request.body));
|
|
139
127
|
const headers = {};
|
|
@@ -176,7 +164,6 @@ var PortlessWebServer = class extends Service {
|
|
|
176
164
|
super(ctx, "webServer");
|
|
177
165
|
this.config = config;
|
|
178
166
|
}
|
|
179
|
-
/** 没有真实监听:报配置值(缺省 0),只用于拼展示用 URL。 */
|
|
180
167
|
get port() {
|
|
181
168
|
return this.config.port ?? 0;
|
|
182
169
|
}
|
|
@@ -225,7 +212,6 @@ var PortlessWebServer = class extends Service {
|
|
|
225
212
|
renderIndex(html) {
|
|
226
213
|
return this.applyIndexTaps(renderIndexInjections(html, this.collectIndexInjections()));
|
|
227
214
|
}
|
|
228
|
-
/** 把一条管道请求分派给注册表;没有匹配也没有 fallback 时回 404。 */
|
|
229
215
|
async dispatch(request) {
|
|
230
216
|
const url = new URL(request.url);
|
|
231
217
|
let pathname;
|
package/lib/wire.js
CHANGED
|
@@ -1,35 +1,19 @@
|
|
|
1
1
|
import { StringDecoder } from "node:string_decoder";
|
|
2
2
|
//#region src/wire.ts
|
|
3
|
-
/**
|
|
4
|
-
* 桌面 host 与 Electron 壳之间的字节管道协议:请求帧(壳 → host,FD 3)与响应帧(host → 壳,FD 4)。
|
|
5
|
-
*
|
|
6
|
-
* 帧头 13 字节:magic(4) | type(1) | streamId(4) | payloadLength(4)。data 帧是原始字节,
|
|
7
|
-
* 其余帧是 JSON 或空载荷。两侧共用这一份实现,避免编解码各写一遍。
|
|
8
|
-
*/
|
|
9
3
|
const DESKTOP_HOST_PROTOCOL_VERSION = 1;
|
|
10
|
-
/** 桌面流的宿主路径:页面/主进程都用它(两侧共享一份,避免路径写两处)。 */
|
|
11
4
|
const DESKTOP_STREAM_PATH = "/.dsh/remote-stream";
|
|
12
|
-
/**
|
|
13
|
-
* 桌面流请求体的首行:定下这条逻辑流的 endpoint 与 payload,后续行都是上行项
|
|
14
|
-
* (客户端 → 宿主;`$events` 这类宿主自己的流被上游释放,不会读到它们)。
|
|
15
|
-
*/
|
|
16
5
|
function encodeDesktopStreamOpen(endpoint, payload) {
|
|
17
6
|
return `${JSON.stringify({
|
|
18
7
|
endpoint,
|
|
19
8
|
payload
|
|
20
9
|
})}\n`;
|
|
21
10
|
}
|
|
22
|
-
/** 桌面流请求体里的一道上行项。 */
|
|
23
11
|
function encodeDesktopStreamItem(item) {
|
|
24
12
|
return `${JSON.stringify(item)}\n`;
|
|
25
13
|
}
|
|
26
|
-
/** 壳写请求帧的管道描述符。 */
|
|
27
14
|
const DESKTOP_REQUEST_PIPE_FD = 3;
|
|
28
|
-
/** host 写响应帧的管道描述符。 */
|
|
29
15
|
const DESKTOP_RESPONSE_PIPE_FD = 4;
|
|
30
|
-
/** Node 生命周期 IPC 通道的描述符(ready / fatal / shutdown)。 */
|
|
31
16
|
const DESKTOP_CONTROL_IPC_FD = 5;
|
|
32
|
-
/** 单个 data 帧携带的最大原始字节数。 */
|
|
33
17
|
const DESKTOP_PIPE_CHUNK_BYTES = 65536;
|
|
34
18
|
const FRAME_MAGIC = 1146308659;
|
|
35
19
|
const FRAME_HEADER_BYTES = 13;
|
|
@@ -66,35 +50,27 @@ function encodeFrame(type, streamId, payload, dataType, direction) {
|
|
|
66
50
|
function jsonPayload(type, streamId, value, dataType, direction) {
|
|
67
51
|
return encodeFrame(type, streamId, Buffer.from(JSON.stringify(value), "utf8"), dataType, direction);
|
|
68
52
|
}
|
|
69
|
-
/** 编码开启一个请求流的元数据帧(壳侧)。 */
|
|
70
53
|
function encodeDesktopRequestStart(streamId, request) {
|
|
71
54
|
return jsonPayload(REQUEST_FRAME_START, streamId, request, REQUEST_FRAME_DATA, "request");
|
|
72
55
|
}
|
|
73
|
-
/** 编码一个受限的请求体原始分片(壳侧)。 */
|
|
74
56
|
function encodeDesktopRequestData(streamId, data) {
|
|
75
57
|
return encodeFrame(REQUEST_FRAME_DATA, streamId, Buffer.from(data), REQUEST_FRAME_DATA, "request");
|
|
76
58
|
}
|
|
77
|
-
/** 编码请求体正常结束(壳侧)。 */
|
|
78
59
|
function encodeDesktopRequestEnd(streamId) {
|
|
79
60
|
return encodeFrame(REQUEST_FRAME_END, streamId, Buffer.alloc(0), REQUEST_FRAME_DATA, "request");
|
|
80
61
|
}
|
|
81
|
-
/** 编码取消一个请求及其响应(壳侧)。 */
|
|
82
62
|
function encodeDesktopRequestCancel(streamId) {
|
|
83
63
|
return encodeFrame(REQUEST_FRAME_CANCEL, streamId, Buffer.alloc(0), REQUEST_FRAME_DATA, "request");
|
|
84
64
|
}
|
|
85
|
-
/** 编码响应元数据帧(host 侧)。 */
|
|
86
65
|
function encodeDesktopResponseStart(streamId, response) {
|
|
87
66
|
return jsonPayload(RESPONSE_FRAME_START, streamId, response, RESPONSE_FRAME_DATA, "response");
|
|
88
67
|
}
|
|
89
|
-
/** 编码一个受限的响应体原始分片(host 侧)。 */
|
|
90
68
|
function encodeDesktopResponseData(streamId, data) {
|
|
91
69
|
return encodeFrame(RESPONSE_FRAME_DATA, streamId, Buffer.from(data), RESPONSE_FRAME_DATA, "response");
|
|
92
70
|
}
|
|
93
|
-
/** 编码响应正常结束(host 侧)。 */
|
|
94
71
|
function encodeDesktopResponseEnd(streamId) {
|
|
95
72
|
return encodeFrame(RESPONSE_FRAME_END, streamId, Buffer.alloc(0), RESPONSE_FRAME_DATA, "response");
|
|
96
73
|
}
|
|
97
|
-
/** 编码一次响应失败,不把 Error 对象跨进程传递(host 侧)。 */
|
|
98
74
|
function encodeDesktopResponseError(streamId, message) {
|
|
99
75
|
return jsonPayload(RESPONSE_FRAME_ERROR, streamId, { message }, RESPONSE_FRAME_DATA, "response");
|
|
100
76
|
}
|
|
@@ -120,19 +96,13 @@ function parseJson(payload, subject) {
|
|
|
120
96
|
function assertEmpty(payload, subject) {
|
|
121
97
|
if (payload.byteLength !== 0) throw new Error(`dsh desktop: ${subject} frame carried a payload`);
|
|
122
98
|
}
|
|
123
|
-
/**
|
|
124
|
-
* 增量解码桌面流请求体:按行取 JSON,首行是 `open`(endpoint / payload),后续每行是一道上行项。
|
|
125
|
-
* 字节边界可能切开多字节字符,所以先过 `StringDecoder` 再切行。
|
|
126
|
-
*/
|
|
127
99
|
var DesktopStreamBodyDecoder = class {
|
|
128
100
|
text = new StringDecoder("utf8");
|
|
129
101
|
buffer = "";
|
|
130
|
-
/** 追加字节并返回所有已完整的行的解析值(空行忽略)。 */
|
|
131
102
|
push(chunk) {
|
|
132
103
|
this.buffer += this.text.write(Buffer.from(chunk));
|
|
133
104
|
return this.take(false);
|
|
134
105
|
}
|
|
135
|
-
/** 请求体结束:交付末行(没有换行收尾时也存在)。 */
|
|
136
106
|
finish() {
|
|
137
107
|
this.buffer += this.text.end();
|
|
138
108
|
return this.take(true);
|
|
@@ -161,10 +131,8 @@ function parseStreamLine(line) {
|
|
|
161
131
|
throw new Error(`dsh desktop: stream body line is not JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
162
132
|
}
|
|
163
133
|
}
|
|
164
|
-
/** 增量解码 host 侧从请求管道收到的请求帧。 */
|
|
165
134
|
var DesktopHostRequestDecoder = class {
|
|
166
135
|
buffer = Buffer.alloc(0);
|
|
167
|
-
/** 追加字节并返回所有已完整的请求帧。 */
|
|
168
136
|
push(chunk) {
|
|
169
137
|
this.buffer = this.buffer.byteLength === 0 ? chunk : Buffer.concat([this.buffer, chunk]);
|
|
170
138
|
const frames = [];
|
|
@@ -174,7 +142,6 @@ var DesktopHostRequestDecoder = class {
|
|
|
174
142
|
frames.push(frame);
|
|
175
143
|
}
|
|
176
144
|
}
|
|
177
|
-
/** EOF 落在帧中间时报错。 */
|
|
178
145
|
finish() {
|
|
179
146
|
if (this.buffer.byteLength !== 0) throw new Error("dsh desktop: request pipe ended inside a frame");
|
|
180
147
|
}
|
|
@@ -222,10 +189,8 @@ var DesktopHostRequestDecoder = class {
|
|
|
222
189
|
};
|
|
223
190
|
}
|
|
224
191
|
};
|
|
225
|
-
/** 增量解码壳侧从响应管道收到的响应帧。 */
|
|
226
192
|
var DesktopHostResponseDecoder = class {
|
|
227
193
|
buffer = Buffer.alloc(0);
|
|
228
|
-
/** 追加字节并返回所有已完整的响应帧。 */
|
|
229
194
|
push(chunk) {
|
|
230
195
|
this.buffer = this.buffer.byteLength === 0 ? chunk : Buffer.concat([this.buffer, chunk]);
|
|
231
196
|
const frames = [];
|
|
@@ -235,7 +200,6 @@ var DesktopHostResponseDecoder = class {
|
|
|
235
200
|
frames.push(frame);
|
|
236
201
|
}
|
|
237
202
|
}
|
|
238
|
-
/** EOF 落在帧中间时报错。 */
|
|
239
203
|
finish() {
|
|
240
204
|
if (this.buffer.byteLength !== 0) throw new Error("dsh desktop: response pipe ended inside a frame");
|
|
241
205
|
}
|
|
@@ -286,14 +250,12 @@ var DesktopHostResponseDecoder = class {
|
|
|
286
250
|
return { message: value.message };
|
|
287
251
|
}
|
|
288
252
|
};
|
|
289
|
-
/** IPC 事件校验(壳侧)。 */
|
|
290
253
|
function isDesktopHostEvent(value) {
|
|
291
254
|
if (!isRecord(value)) return false;
|
|
292
255
|
if (value.type === "ready") return value.protocolVersion === 1;
|
|
293
256
|
if (value.type === "fatal") return typeof value.message === "string" && (value.diagnostic === void 0 || typeof value.diagnostic === "string");
|
|
294
257
|
return false;
|
|
295
258
|
}
|
|
296
|
-
/** IPC 命令校验(host 侧)。 */
|
|
297
259
|
function isDesktopHostCommand(value) {
|
|
298
260
|
return isRecord(value) && value.type === "shutdown";
|
|
299
261
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/dsh-desktop-host",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Desktop host process for dsh-desktopify: boots the desktop profile and serves it through an in-process, portless webServer over byte pipes — upstream apps/desktop-host with the web carrier taken over and the Office
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Desktop host process for dsh-desktopify: boots the desktop profile and serves it through an in-process, portless webServer over byte pipes — upstream apps/desktop-host with the web carrier taken over and the Office composition dropped.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"desktop",
|
|
7
7
|
"dsh",
|
|
@@ -32,9 +32,7 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@deepseek-ai/dsh": "^0.2.0-rc.1",
|
|
34
34
|
"@deepseek-ai/dsh-app-boot": "^0.2.0-rc.1",
|
|
35
|
-
"@deepseek-ai/dsh-home-paths": "^0.2.0-rc.1",
|
|
36
35
|
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
|
|
37
|
-
"@deepseek-ai/dsh-tool-workspace-dependencies": "^0.2.0-rc.1",
|
|
38
36
|
"@deepseek-ai/schemastery": "^3.18.4"
|
|
39
37
|
},
|
|
40
38
|
"devDependencies": {
|