@morlay/dsh-client-modules 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 morlay
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,24 @@
1
- # Temporary Holding Version
1
+ # @morlay/dsh-client-modules
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 客户端的**联邦预编译**链:按请求触发、按包产出,把三方包与仓库内的包统一成带版本的地址
4
+ (`/@client-modules/<pkg>@<version>/<export>`),dev 与打包形态共用同一条链、同一套插件。
5
+
6
+ - **dev**:宿主内起 vite(middleware 模式),裸说明符指向联邦地址、正在改的包逐文件走(HMR);
7
+ - **打包**:直接跑 rolldown 产出到 profile 的缓存目录,路由按产物清单(`.files.json`)服务。
8
+
9
+ 判据、分工与坑都在 [设计:客户端资源的联邦式预编译](./.agents/designs/20261013-客户端资源的联邦式预编译.md)
10
+ (URL 空间、出口名↔文件名、版本表、拆分配下限、两种形态的差异与 `--prod` 的验证面)。
11
+
12
+ 被 [`@local/dshdev`](../../../devpackages/dshdev/README.md) 经 `@morlay/web-custom-profile` 挂进 profile;上游内部
13
+ 文件按需 vendored 到各包的 `src/vendor/`(同步工具见 `@local/upstream-sync`)。
14
+
15
+ ## 浏览器侧:只补上游的 `modules` 服务面
16
+
17
+ 本包的 client 半不做别的事——它只把上游 `modules` 服务补上(`entries.releaseChange` 恒 `undefined`、
18
+ `dismissRelease()` 与 `invalidate()` 空操作),见
19
+ [`src/client/index.ts`](./src/client/index.ts) 的文件头。
20
+
21
+ 原因:上游 `ui-settings-general`(侧栏左下角那个设置入口的占位者)从 0.2.x 起 inject `modules`,
22
+ 而上游 `client-modules` 行已被 `web-custom-profile` 停用(模块表 roster 与 `/plugins` 路由由本链取代),
23
+ 浏览器侧于是没有这个服务——那一行永远等不到 inject 满足,设置入口整个消失。**停用一行就要补上它对外
24
+ 承诺的服务面**,这是本包必须带着 client 半的原因。
@@ -0,0 +1,187 @@
1
+ /* 挂载默认样式:`html, body, #root` 撑满、字体栈、灰度抗锯齿与基础配色。
2
+ *
3
+ * 这几条本来由上游 shell 的挂载面带来(`@deepseek-ai/dsh-client-web/src/base.css`),而我们自研入口
4
+ * 不走 shell——那就**自己带一份**:包名是唯一的寻址入口,而 `dsh-client-web` 是上游 web 应用的包,
5
+ * 不在我们的部署闭包(`@deepseek-ai/dsh-client-web` 解析成 `0.0.0` 就是没装)。语义照它抄,
6
+ * 主题 token(`--dsw-*`)仍由 `ui-theme` 的 client 半在运行时注入——这里的回退值只是"注入之前别闪"。 */
7
+
8
+ html,
9
+ body,
10
+ #root {
11
+ height: 100%;
12
+ margin: 0;
13
+ }
14
+
15
+ body {
16
+ font-family: var(
17
+ --dsw-font-family,
18
+ -apple-system,
19
+ BlinkMacSystemFont,
20
+ "Segoe UI",
21
+ "PingFang SC",
22
+ "Hiragino Sans GB",
23
+ "Microsoft YaHei",
24
+ "Helvetica Neue",
25
+ Helvetica,
26
+ Arial,
27
+ sans-serif
28
+ );
29
+ /* 灰度抗锯齿优于次像素渲染:WebKit/Blink 与 Firefox 在 macOS 上的等价写法,其余引擎忽略这两行。 */
30
+ -webkit-font-smoothing: antialiased;
31
+ -moz-osx-font-smoothing: grayscale;
32
+ color: var(--dsw-alias-label-primary, #0f1115);
33
+ background: var(--dsw-alias-bg-base, #fff);
34
+ }
35
+
36
+ /* ---- 启动页(`boot-page.ts`):与上游 `@deepseek-ai/dsh-client-web/src/boot-page.module.css` 一致。
37
+ *
38
+ * 这份**不能**依赖主题注入成功——它的职责正是"主题/插件还没到"的那段;`--dsw-*` 有就取,没有就用这里的
39
+ * 回退值(深色模式按 `body[data-ds-dark-theme]` 换一组)。 */
40
+
41
+ .dsh-boot {
42
+ --dsh-boot-label-primary: #0f1115;
43
+ --dsh-boot-label-secondary: #61666b;
44
+ --dsh-boot-label-tertiary: #81858c;
45
+ --dsh-boot-border: rgb(0 0 0 / 10%);
46
+ --dsh-boot-brand: #0f1115;
47
+
48
+ height: 100%;
49
+ display: grid;
50
+ place-items: center;
51
+ background: var(--dsw-alias-bg-base, var(--dsh-boot-bg, Canvas));
52
+ }
53
+
54
+ body[data-ds-dark-theme] .dsh-boot {
55
+ --dsh-boot-label-primary: #f9fafb;
56
+ --dsh-boot-label-secondary: #cfd3d6;
57
+ --dsh-boot-label-tertiary: #adb2b8;
58
+ --dsh-boot-border: rgb(255 255 255 / 12%);
59
+ --dsh-boot-brand: #f9fafb;
60
+ }
61
+
62
+ .dsh-boot-card {
63
+ display: flex;
64
+ flex-direction: column;
65
+ align-items: center;
66
+ gap: 16px;
67
+ }
68
+
69
+ .dsh-boot-wordmark {
70
+ font-size: 16px;
71
+ line-height: 24px;
72
+ font-weight: 600;
73
+ letter-spacing: 0.08em;
74
+ color: var(--dsw-alias-label-primary, var(--dsh-boot-label-primary));
75
+ }
76
+
77
+ .dsh-boot-hint {
78
+ font-size: 12px;
79
+ line-height: 18px;
80
+ color: var(--dsw-alias-label-tertiary, var(--dsh-boot-label-tertiary));
81
+ }
82
+
83
+ .dsh-boot-spinner {
84
+ position: relative;
85
+ width: 20px;
86
+ height: 20px;
87
+ border-radius: 50%;
88
+ corner-shape: round;
89
+ border: 2px solid var(--dsw-alias-border-l2, var(--dsh-boot-border));
90
+ animation: dsh-boot-spin 0.8s linear infinite;
91
+ }
92
+
93
+ .dsh-boot-spinner::after {
94
+ content: "";
95
+ position: absolute;
96
+ inset: -2px;
97
+ border-radius: inherit;
98
+ background: conic-gradient(
99
+ var(--dsw-alias-brand-primary, var(--dsh-boot-brand)) var(--dsh-boot-arc, 72deg),
100
+ transparent 0
101
+ );
102
+ -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 0);
103
+ mask: radial-gradient(farthest-side, transparent calc(100% - 2px), #000 0);
104
+ }
105
+
106
+ @keyframes dsh-boot-spin {
107
+ to {
108
+ transform: rotate(360deg);
109
+ }
110
+ }
111
+
112
+ .dsh-boot-failed {
113
+ max-width: 480px;
114
+ display: flex;
115
+ flex-direction: column;
116
+ gap: 8px;
117
+ }
118
+
119
+ .dsh-boot-failed-title {
120
+ font-size: 14px;
121
+ line-height: 22px;
122
+ font-weight: 600;
123
+ color: var(--dsw-alias-label-primary, var(--dsh-boot-label-primary));
124
+ }
125
+
126
+ .dsh-boot-failed-item {
127
+ font-family: var(--ds-font-family-code, "SF Mono", "JetBrains Mono", Consolas, Menlo, monospace);
128
+ font-size: 12px;
129
+ line-height: 18px;
130
+ color: var(--dsw-alias-label-secondary, var(--dsh-boot-label-secondary));
131
+ white-space: pre-wrap;
132
+ }
133
+
134
+ /* ---- macOS 桌面:**唯一的拖拽声明**(与上游 `@deepseek-ai/dsh-client-web/src/base.css` 一致)。
135
+ *
136
+ * 我们不走上游 shell,这几条得自己带。一条 chrome row 在 markup 里标 `data-window-drag`,它自己的盒子就是
137
+ * 可拖拽几何(不必再让某条固定高度的带子去对齐)——侧边栏顶部那条 strip 就是这么标的:在 macOS 上拖它
138
+ * 即拖窗口,而它上面的按钮照常点击(这就是"穿透")。
139
+ *
140
+ * Electron 按**几何**合成 app-region 而不是命中测试,所以盖在 row 上的浮层要自己减掉:`body` 的直接子元素
141
+ * (除 `#root`)与 `#root` 里的交互控件(原生元素、ARIA role、tabindex 的自定义件)一条选择器统一减,
142
+ * 不必每个组件各自 opt-out。`data-window-drag-recall` 是"某条 row 的盒子动过、要 Electron 重收拖拽矩形"
143
+ * 的那一帧脉冲(electron#32341)。 */
144
+
145
+ html[data-platform="darwin"] [data-window-drag] {
146
+ -webkit-app-region: drag;
147
+ }
148
+
149
+ html[data-platform="darwin"] [data-window-drag-recall] {
150
+ -webkit-app-region: no-drag;
151
+ }
152
+
153
+ html[data-platform="darwin"] body > :not(#root) {
154
+ -webkit-app-region: no-drag;
155
+ }
156
+
157
+ html[data-platform="darwin"]
158
+ :is(
159
+ button,
160
+ a,
161
+ input,
162
+ select,
163
+ textarea,
164
+ summary,
165
+ [contenteditable="true"],
166
+ [tabindex],
167
+ [role="dialog"],
168
+ [role="alertdialog"],
169
+ [role="menu"],
170
+ [role="listbox"],
171
+ [role="tooltip"],
172
+ [role="button"],
173
+ [role="link"],
174
+ [role="tab"],
175
+ [role="menuitem"],
176
+ [role="menuitemcheckbox"],
177
+ [role="menuitemradio"],
178
+ [role="option"],
179
+ [role="checkbox"],
180
+ [role="radio"],
181
+ [role="switch"],
182
+ [role="slider"],
183
+ [role="combobox"],
184
+ [role="textbox"]
185
+ ) {
186
+ -webkit-app-region: no-drag;
187
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * 启动页与失败报告:**framework-free**,与上游 `@deepseek-ai/dsh-client-web/src/boot-page` 一致。
3
+ *
4
+ * 插件开始注册到 UI 渲染器接管挂载点之间,页面上先是它——React 要等 `ui-renderer` 才到,那之前必须有个
5
+ * 不依赖框架的东西能把"正在加载"与"失败了"显示出来(否则就是白屏,而原因只有 console 里看得见)。
6
+ *
7
+ * @module @morlay/dsh-client-modules/app-boot/boot-page
8
+ */
9
+ /** 装配期一行的状态投影(对应上游的 `LoaderEntryState`,这里只关心这四态)。 */
10
+ export type BootEntryState = "pending" | "loading" | "active" | "failed";
11
+
12
+ /** Create a div with one module class and optional text. */
13
+ function div(className: string | undefined, text?: string): HTMLDivElement {
14
+ const el = document.createElement("div");
15
+ el.className = className ?? "";
16
+ if (text !== undefined) el.textContent = text;
17
+ return el;
18
+ }
19
+
20
+ /** 内核自己的启动页:挂在应用挂载点里面,直到 UI 渲染器接管。 */
21
+ export class BootPage {
22
+ private readonly root: HTMLDivElement;
23
+ private readonly card: HTMLDivElement;
24
+ private readonly wordmark: HTMLDivElement;
25
+ private readonly spinner: HTMLDivElement;
26
+ private readonly hint: HTMLDivElement;
27
+ private readonly states = new Map<string, BootEntryState>();
28
+ private readonly active = new Set<string>();
29
+ private total = 0;
30
+ private failure: string | undefined;
31
+
32
+ /**
33
+ * Build and attach the boot page.
34
+ *
35
+ * @param container - Application mount point.
36
+ */
37
+ constructor(container: HTMLElement) {
38
+ this.root = div("dsh-boot");
39
+ this.root.dataset["dshBoot"] = "";
40
+ this.card = div("dsh-boot-card");
41
+ this.wordmark = div("dsh-boot-wordmark", "HARNESS");
42
+ this.spinner = div("dsh-boot-spinner");
43
+ this.spinner.dataset["dshBootSpinner"] = "";
44
+ this.hint = div("dsh-boot-hint", "Loading plugins…");
45
+ this.card.append(this.wordmark, this.spinner, this.hint);
46
+ this.root.append(this.card);
47
+ container.append(this.root);
48
+ this.updateProgress();
49
+ }
50
+
51
+ /**
52
+ * Set the number of assembly rows represented by the progress arc.
53
+ *
54
+ * @param total - Complete boot roster size.
55
+ */
56
+ setTotal(total: number): void {
57
+ this.total = total;
58
+ this.updateProgress();
59
+ }
60
+
61
+ /**
62
+ * Project one row's state.
63
+ *
64
+ * @param id - Row (package) name.
65
+ * @param state - Projected state.
66
+ */
67
+ setState(id: string, state: BootEntryState): void {
68
+ this.states.set(id, state);
69
+ if (state === "active") this.active.add(id);
70
+ this.updateProgress();
71
+ this.render();
72
+ }
73
+
74
+ /**
75
+ * Display the boot failure report.
76
+ *
77
+ * @param message - Failure report text.
78
+ */
79
+ fail(message: string): void {
80
+ this.failure = message;
81
+ this.render();
82
+ }
83
+
84
+ /** Detach the page before or after the UI renderer takes the mount point. */
85
+ dispose(): void {
86
+ this.root.remove();
87
+ }
88
+
89
+ /** Redraw the state-dependent content below the wordmark. */
90
+ private render(): void {
91
+ const failed = [...this.states].filter(([, state]) => state === "failed").map(([id]) => id);
92
+ if (this.failure === undefined && failed.length === 0) {
93
+ if (this.spinner.parentElement !== this.card) {
94
+ this.card.replaceChildren(this.wordmark, this.spinner, this.hint);
95
+ }
96
+ return;
97
+ }
98
+ const report = div("dsh-boot-failed");
99
+ report.append(div("dsh-boot-failed-title", "Failed to load plugins"));
100
+ for (const id of failed) report.append(div("dsh-boot-failed-item", id));
101
+ if (this.failure !== undefined) report.append(div("dsh-boot-failed-item", this.failure));
102
+ this.card.replaceChildren(this.wordmark, report);
103
+ }
104
+
105
+ /** Grow the rotating arc monotonically as rows activate. */
106
+ private updateProgress(): void {
107
+ const ratio = this.total === 0 ? 0 : Math.min(this.active.size / this.total, 1);
108
+ this.spinner.style.setProperty("--dsh-boot-arc", `${String(Math.round(72 + ratio * 216))}deg`);
109
+ }
110
+ }
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>DSH Local Build</title>
7
+ </head>
8
+ <body>
9
+ <!-- 容器与上游一致:`#root`(`apps/web/index.html`)。挂载点换名字会让上游按 `#root` 写的样式失效。 -->
10
+ <div id="root"></div>
11
+ <!-- vite 的入口:它自己会把 index.ts 转译、解析依赖、并挂上 HMR。这里是 **root 相对**路径
12
+ (vite 的 root 就是这个目录)——HTML 里的 `src` 是 URL,不走 alias 解析。 -->
13
+ <script type="module" src="./index.ts"></script>
14
+ </body>
15
+ </html>
@@ -0,0 +1,101 @@
1
+ /**
2
+ * dev 客户端的应用入口(真实文件,由 vite 当普通入口处理)。
3
+ *
4
+ * 它只做装配:从虚拟模块 `virtual:dsh-client-rows` 拿行清单(各 client 行的源码入口由插件解析),
5
+ * 按 `dsh.client.inject` 的顺序 `ctx.plugin(...)`,最后等 `root` slot 就绪再挂载。
6
+ *
7
+ * 页面 HTML 与入口都是**真实文件**——vite 的 root 指向仓库根,它按自己的机制转译、预打包依赖、挂 HMR,
8
+ * 我们不再自己生成脚本、也不再自己拦路由。
9
+ */
10
+ // 品牌字体样式:上游的 Web 入口同样单独引这一份(`apps/web/src/main.ts`);其余全局样式表由 `ui-theme`
11
+ // 的 client 半用 `?inline` 在运行时注入。
12
+ //
13
+ // **按包名引**(与上游 `main.ts` 一致),不写 `@/vendor/…` 这类仓库路径:包名是唯一的寻址入口,走哪条
14
+ // 链(联邦产物 / 源码)由解析规则决定——写仓库路径等于把 vendor 的目录布局抄进代码,还会把一份样式
15
+ // 拆成一串 `/vendor/…` 的逐文件请求。
16
+ import "@deepseek-ai/dsh-client-ui-theme/brand-font.css";
17
+ // 挂载默认样式(`html, body, #root { height: 100%; margin: 0 }` 与 body 的字体/颜色/抗锯齿):这份
18
+ // 自己带——上游由 shell 的 `client/web/src/boot.ts` 引入,而**我们不走 shell**;`dsh-client-web` 是上游
19
+ // web 应用的包,不在我们的部署闭包里(按包名引会解析成 `0.0.0` 而 404)。
20
+ import "./base.css";
21
+ import { BootPage } from "./boot-page.ts";
22
+ import { Context } from "@deepseek-ai/cordis";
23
+ // @ts-expect-error 虚拟模块由 devkit 的 vite 插件提供
24
+ import rows from "dsh-client-rows";
25
+
26
+ declare global {
27
+ // eslint-disable-next-line no-var -- 调试面:e2e 与人工排查都从这里读装配结果。
28
+ var __DSH_CTX__: Context | undefined;
29
+ }
30
+
31
+ const ctx = new Context();
32
+ globalThis.__DSH_CTX__ = ctx;
33
+ console.log("[dsh] entry: context created", rows.length, "rows");
34
+
35
+ // cordis 把 fiber 内的失败交给 `ctx.logger.error`,而它在浏览器里默认不落 console —— 接到 console,
36
+ // 装配期的失败才看得见(否则只有"页面空白 + 无报错")。
37
+ const originalError = ctx.logger?.error?.bind(ctx.logger);
38
+ if (ctx.logger !== undefined) {
39
+ ctx.logger.error = (...args) => {
40
+ console.error("[dsh] cordis error", ...args);
41
+ if (originalError !== undefined) originalError(...args);
42
+ };
43
+ }
44
+
45
+ // 容器与上游一致:`#root`(`apps/web/index.html`)。挂载点与启动页都在注册**之前**取好。
46
+ const container = document.getElementById("root");
47
+ if (container === null) throw new Error("[dsh] entry: #root 挂载点不存在");
48
+
49
+ // **启动页**(与上游一致):React 要等 `ui-renderer` 才到,那之前由它显示"正在加载 / 失败了"——没有它就是
50
+ // 白屏,而失败原因只在 console 里。它挂在 `#root` 里面,渲染器接管时摘掉。
51
+ const boot = new BootPage(container);
52
+ boot.setTotal(rows.length);
53
+
54
+ // **按模块身份去重**:行清单里可能出现两条指向同一个包的 entry(包名写法不同,如带 `/client` 后缀),
55
+ // 那样同一个模块会被 `ctx.plugin` 两次——第二次 `apply` 就撞上"同 namespace 的 locale 已注册"这类
56
+ // 重复注册断言。
57
+ const plugged = new Set<unknown>();
58
+ for (const [index, row] of rows.entries()) {
59
+ const mod = row.module;
60
+ if (plugged.has(mod)) {
61
+ console.warn("[dsh] 同一模块出现在多行,跳过", row.id);
62
+ continue;
63
+ }
64
+ plugged.add(mod);
65
+ // client 行的导出有两种形状:`{ apply, inject }` 与 `{ default: X }`。整块交给 cordis——不要自己拼
66
+ // `{ apply: factory.apply }`:default 形状上没有 `apply`,那样会取到 `Function.prototype.apply`。
67
+ const plugin = typeof mod.apply === "function" ? mod : (mod.default ?? undefined);
68
+ if (plugin === undefined) {
69
+ console.warn("[dsh] skip (no apply/default)", row.id);
70
+ continue;
71
+ }
72
+ try {
73
+ console.log("[dsh] plugin", index, row.id, "inject:", JSON.stringify(plugin.inject ?? []));
74
+ ctx.plugin(plugin, { name: plugin.name ?? row.id });
75
+ boot.setState(row.id, "active");
76
+ } catch (error) {
77
+ console.error("[dsh] plugin failed", row.id, error);
78
+ boot.setState(row.id, "failed");
79
+ boot.fail(String(error));
80
+ }
81
+ }
82
+ console.log("[dsh] entry: plugins registered");
83
+
84
+ // 容器与上游一致:`#root`(`apps/web/index.html`)。
85
+
86
+ // 等 **'root' slot 真的注册**再挂载:`layout` 服务在注册 slot 之前就已提供(`provide('layout')` 早于
87
+ // `slots.register({name:'root'})`),所以等 uiRenderer 就绪不够——那时挂载会渲染一棵空树,之后注册也不会补渲。
88
+ // 上层(`ui-layout` 的 apply)是异步的,这里给一个上限轮询。
89
+ for (let attempt = 0; attempt < 300; attempt += 1) {
90
+ if ((ctx.slots?.entries?.("root")?.length ?? 0) > 0) break;
91
+ await new Promise((resolve) => setTimeout(resolve, 20));
92
+ }
93
+ console.log("[dsh] entry: root slot ready");
94
+
95
+ // 渲染器接管挂载点:启动页摘掉(挂在它里面会跟着一起被替换,这里显式收尾)。
96
+ boot.dispose();
97
+
98
+ await ctx.inject(["uiRenderer"], (scope) => {
99
+ scope.effect(() => scope.uiRenderer.mount(container), "dev entry: application mount");
100
+ });
101
+ console.log("[dsh] entry: mounted");
@@ -0,0 +1,19 @@
1
+ // 联邦构建(rolldown,CJS 产物)用的 `node:*` 替身。
2
+ //
3
+ // 与同目录的 `node-stub.ts`(dev 逐文件服务用的 ESM 形态)**同一语义**:这些 API 在浏览器里本不该被
4
+ // 调用,但模块求值不该中断。形态为什么要分两份:ESM 的具名导入在构建期要**校验存在性**(缺了就
5
+ // `MISSING_EXPORT`),而 CJS 不校验——联邦产物的 `require("node:fs")` 需要的就是后者。
6
+ //
7
+ // 缺了这个文件时,构建报的是 `UNLOADABLE_DEPENDENCY`,整包构建失败、页面上那一路直接 404。
8
+ module.exports = {
9
+ Readable: class {},
10
+ Writable: class {},
11
+ Duplex: class {},
12
+ Transform: class {},
13
+ PassThrough: class {},
14
+ pipeline: (...args) => {
15
+ const done = args.at(-1);
16
+ if (typeof done === "function") done();
17
+ },
18
+ promises: {},
19
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 浏览器里的 `node:*` 替身。
3
+ *
4
+ * 宿主半的代码有时被**同包的相对导入**拖进 client 图(典型是 `client/connection` 的 client 半引到
5
+ * `src/loopback-hostname.ts`、再链到 `src/http-bridge.ts`,那里 `import { Readable } from 'node:stream'`)。
6
+ * vite 默认把 `node:*` 换成 browser-external 代理,**读取成员时抛错**,于是一整条入口求值中断——页面白屏
7
+ * 且只有一条难以归因的报错。这里给出求值安全的空实现:这些 API 在浏览器里本不该被调用,但模块求值不应中断。
8
+ *
9
+ * **两份形态、同一语义**:dev 形态逐文件服务,缺失的具名导入只是 undefined(运行期),这份 ESM 够用;
10
+ * 联邦构建走 rolldown,它把"没有该具名导出"当**构建错误**(`MISSING_EXPORT`),那里得用 CJS 形态的
11
+ * `node-stub.cjs`——CJS 的具名导入不校验存在性。
12
+ */
13
+ export class Readable {}
14
+ export class Writable {}
15
+ export class Duplex {}
16
+ export class Transform {}
17
+ export class PassThrough {}
18
+ export const pipeline = (...args: unknown[]): void => {
19
+ const done = args.at(-1);
20
+ if (typeof done === "function") (done as () => void)();
21
+ };
22
+ export const promises = {};
23
+ export default { Readable, Writable, Duplex, Transform, PassThrough, pipeline, promises };
@@ -0,0 +1,65 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@morlay/dsh-client-modules",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region src/client/index.ts
8
+ /** 恒 `undefined` 的可订阅快照(`useSyncExternalStore` 的入参形状)。 */
9
+ const NO_RELEASE_CHANGE = {
10
+ getSnapshot: () => void 0,
11
+ subscribe: () => () => {}
12
+ };
13
+ /**
14
+ * 浏览器侧的根插件。
15
+ *
16
+ * @param ctx - 浏览器侧根上下文。
17
+ */
18
+ function apply(ctx) {
19
+ ctx.reflect.provide("modules", {
20
+ entries: {
21
+ releaseChange: NO_RELEASE_CHANGE,
22
+ dismissRelease: () => {}
23
+ },
24
+ invalidate: () => {}
25
+ });
26
+ installTerminalRecovery(ctx);
27
+ }
28
+ /**
29
+ * 终端恢复兜底:`view` 之前先**对账**,把宿主已经不认的终端 id 当新 tab 处理。
30
+ *
31
+ * 上游 `TerminalView` 的语义是「新 tab 才分配」(`createWhenMissing = saved === undefined`),而
32
+ * `saved = terminalId ?? bindings.get(sessionId, contentId)`:UI 传的 `terminalId` 实测是 `undefined`,
33
+ * 所以决定权全在 `bindings` —— 它不随宿主侧关闭状态失效。清单里没有这个 id(宿主重启过、或那个终端被
34
+ * 关过)时删掉这条 binding,让上游按新 tab 分配;清单里有则原样透传,恢复能力不变。清单**没加载**时按
35
+ * 「不在」处理:那时用户要的就是打开一个终端,让它重新分配比卡在死状态里强。
36
+ *
37
+ * 注意:`bindings` / `inventories` 是上游 Service 的私有字段,`view` 在其原型上(实例属性覆盖无效,
38
+ * 见 cordis 的 Service 代理)——所以这里改的是原型。这是**绕过上游缺陷**,等上游把 binding 与关闭状态
39
+ * 对上账就该删掉。
40
+ *
41
+ * @param ctx - 浏览器侧根上下文。
42
+ */
43
+ function installTerminalRecovery(ctx) {
44
+ ctx.inject(["webTerminals"], (scope) => {
45
+ const terminals = scope.get("webTerminals");
46
+ if (terminals === void 0) return;
47
+ const proto = Object.getPrototypeOf(terminals);
48
+ if (proto.view === void 0 || proto.__terminalRecovery === true) return;
49
+ proto.__terminalRecovery = true;
50
+ const original = proto.view;
51
+ proto.view = function(sessionId, key, contentId, terminalId, shellPath) {
52
+ const bound = terminalId ?? this.bindings?.get?.(sessionId, contentId);
53
+ if (!(bound === void 0 || this.inventories?.get(sessionId)?.items?.has(bound) === true)) {
54
+ this.bindings?.delete?.(sessionId, contentId);
55
+ return original.call(this, sessionId, key, contentId, void 0, shellPath);
56
+ }
57
+ return original.call(this, sessionId, key, contentId, terminalId, shellPath);
58
+ };
59
+ });
60
+ }
61
+ //#endregion
62
+ exports.apply = apply;
63
+ return module.exports;
64
+ }
65
+ });
@@ -0,0 +1,9 @@
1
+ import { Context } from "@deepseek-ai/cordis";
2
+ //#region src/client/index.d.ts
3
+ /**
4
+ * 发布 `modules` 服务(上游同名的服务面,见文件头)。
5
+ *
6
+ * @param ctx - 浏览器侧根上下文。
7
+ */
8
+ export declare function apply(ctx: Context): void;
9
+ //#endregion
@@ -0,0 +1,9 @@
1
+ import { Context } from "@deepseek-ai/cordis";
2
+ //#region src/client/index.d.ts
3
+ /**
4
+ * 发布 `modules` 服务(上游同名的服务面,见文件头)。
5
+ *
6
+ * @param ctx - 浏览器侧根上下文。
7
+ */
8
+ export declare function apply(ctx: Context): void;
9
+ //#endregion
@@ -0,0 +1,56 @@
1
+ //#region src/client/index.ts
2
+ /** 恒 `undefined` 的可订阅快照(`useSyncExternalStore` 的入参形状)。 */
3
+ const NO_RELEASE_CHANGE = {
4
+ getSnapshot: () => void 0,
5
+ subscribe: () => () => {}
6
+ };
7
+ /**
8
+ * 浏览器侧的根插件。
9
+ *
10
+ * @param ctx - 浏览器侧根上下文。
11
+ */
12
+ function apply(ctx) {
13
+ ctx.reflect.provide("modules", {
14
+ entries: {
15
+ releaseChange: NO_RELEASE_CHANGE,
16
+ dismissRelease: () => {}
17
+ },
18
+ invalidate: () => {}
19
+ });
20
+ installTerminalRecovery(ctx);
21
+ }
22
+ /**
23
+ * 终端恢复兜底:`view` 之前先**对账**,把宿主已经不认的终端 id 当新 tab 处理。
24
+ *
25
+ * 上游 `TerminalView` 的语义是「新 tab 才分配」(`createWhenMissing = saved === undefined`),而
26
+ * `saved = terminalId ?? bindings.get(sessionId, contentId)`:UI 传的 `terminalId` 实测是 `undefined`,
27
+ * 所以决定权全在 `bindings` —— 它不随宿主侧关闭状态失效。清单里没有这个 id(宿主重启过、或那个终端被
28
+ * 关过)时删掉这条 binding,让上游按新 tab 分配;清单里有则原样透传,恢复能力不变。清单**没加载**时按
29
+ * 「不在」处理:那时用户要的就是打开一个终端,让它重新分配比卡在死状态里强。
30
+ *
31
+ * 注意:`bindings` / `inventories` 是上游 Service 的私有字段,`view` 在其原型上(实例属性覆盖无效,
32
+ * 见 cordis 的 Service 代理)——所以这里改的是原型。这是**绕过上游缺陷**,等上游把 binding 与关闭状态
33
+ * 对上账就该删掉。
34
+ *
35
+ * @param ctx - 浏览器侧根上下文。
36
+ */
37
+ function installTerminalRecovery(ctx) {
38
+ ctx.inject(["webTerminals"], (scope) => {
39
+ const terminals = scope.get("webTerminals");
40
+ if (terminals === void 0) return;
41
+ const proto = Object.getPrototypeOf(terminals);
42
+ if (proto.view === void 0 || proto.__terminalRecovery === true) return;
43
+ proto.__terminalRecovery = true;
44
+ const original = proto.view;
45
+ proto.view = function(sessionId, key, contentId, terminalId, shellPath) {
46
+ const bound = terminalId ?? this.bindings?.get?.(sessionId, contentId);
47
+ if (!(bound === void 0 || this.inventories?.get(sessionId)?.items?.has(bound) === true)) {
48
+ this.bindings?.delete?.(sessionId, contentId);
49
+ return original.call(this, sessionId, key, contentId, void 0, shellPath);
50
+ }
51
+ return original.call(this, sessionId, key, contentId, terminalId, shellPath);
52
+ };
53
+ });
54
+ }
55
+ //#endregion
56
+ export { apply };