@dsh-workbench/core 0.2.6 → 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.
Files changed (32) hide show
  1. package/README.md +7 -7
  2. package/bin/dsh-workbench +2 -0
  3. package/client.js +252 -36
  4. package/dist/client/client.body.js +252 -36
  5. package/dist/client/index.js +252 -36
  6. package/dist/host/index.js +1054 -737
  7. package/dist/types/app/client.d.ts +21 -15
  8. package/dist/types/app/layouts/rightbar/__tests__/market-order.test.d.ts +1 -0
  9. package/dist/types/app/layouts/rightbar/__tests__/market-running-display.test.d.ts +1 -0
  10. package/dist/types/app/layouts/rightbar/__tests__/market-workbench-act.test.d.ts +1 -0
  11. package/dist/types/app/layouts/rightbar/market-act.d.ts +2 -1
  12. package/dist/types/app/layouts/rightbar/market-rows.d.ts +3 -0
  13. package/dist/types/app/layouts/rightbar/market-settle-hold.d.ts +13 -0
  14. package/dist/types/app/layouts/rightbar/market-settle.d.ts +22 -0
  15. package/dist/types/app/layouts/rightbar/market-transient-state.d.ts +22 -0
  16. package/dist/types/app/layouts/rightbar/market-view-text.d.ts +26 -1
  17. package/dist/types/modules/panels/__tests__/market-tools-harness.d.ts +3 -0
  18. package/dist/types/modules/panels/model/market-workbench-update.d.ts +19 -0
  19. package/dist/types/modules/panels/model/market.d.ts +4 -0
  20. package/dist/types/shared/api/bundle-install.d.ts +1 -0
  21. package/dist/types/shared/api/config-sync.d.ts +0 -2
  22. package/dist/types/shared/api/host/__tests__/post-channels.test.d.ts +1 -0
  23. package/dist/types/shared/api/host/__tests__/self-update.test.d.ts +1 -0
  24. package/dist/types/shared/api/host/bundle-manager.d.ts +1 -0
  25. package/dist/types/shared/api/host/bundle-routes.d.ts +1 -1
  26. package/dist/types/shared/api/host/config-sync.d.ts +2 -2
  27. package/dist/types/shared/api/host/post-channels.d.ts +18 -0
  28. package/dist/types/shared/api/host/self-update.d.ts +45 -0
  29. package/docs/HANDOFF.md +3 -3
  30. package/docs/OPERATIONS.md +33 -3
  31. package/package.json +4 -4
  32. package/scripts/seed-profile.sh +9 -0
@@ -111,6 +111,7 @@ export interface WorkbenchVersion {
111
111
  readonly pkg: string;
112
112
  readonly installed: string;
113
113
  readonly latest?: string;
114
+ readonly running?: string;
114
115
  readonly local?: boolean;
115
116
  }
116
117
  export interface ModuleVersionsPayload {
@@ -22,13 +22,11 @@ export interface ConfigSyncSnapshot {
22
22
  readonly profiles: readonly ConfigSyncProfile[];
23
23
  readonly rows: readonly ConfigSyncRow[];
24
24
  readonly emptyProviders: boolean;
25
- readonly restart: true;
26
25
  readonly reason?: string;
27
26
  }
28
27
  export interface ConfigSyncApplied {
29
28
  readonly written: readonly string[];
30
29
  readonly skipped: readonly string[];
31
- readonly restart: true;
32
30
  }
33
31
  export interface ConfigSyncRequest {
34
32
  readonly source: string;
@@ -5,6 +5,7 @@ export interface BundleManager {
5
5
  inspect(spec: string, registry?: BundleRegistry): Promise<BundleInspection | null>;
6
6
  install(request: BundleInstallRequest): Promise<BundleChange | null>;
7
7
  remove(name: string): Promise<BundleChange | null>;
8
+ setEnabled?(name: string, enabled: boolean): Promise<BundleChange | null>;
8
9
  cancel(requestId: string): Promise<BundleCancellation | null>;
9
10
  }
10
11
  export declare function readBundleManager(ctx: DshHostContext): BundleManager | undefined;
@@ -3,5 +3,5 @@ import { type RouteHandler } from '@dsh-workbench/shared/host';
3
3
  export declare function bundleRegistriesHandler(ctx: DshHostContext): RouteHandler;
4
4
  export declare function resetRefusalLog(): void;
5
5
  export declare function resetHoldLog(): void;
6
- export declare function moduleVersionsHandler(ctx: DshHostContext, shell: DshShell | undefined): RouteHandler;
6
+ export declare function moduleVersionsHandler(ctx: DshHostContext, shell: DshShell | undefined, running?: string | undefined): RouteHandler;
7
7
  export declare function bundleInspectHandler(ctx: DshHostContext): RouteHandler;
@@ -1,4 +1,4 @@
1
- import type { RouteHandler } from '@dsh-workbench/shared/host';
1
+ import type { PostResource, RouteHandler } from '@dsh-workbench/shared/host';
2
2
  import type { ConfigSyncRow, ConfigSyncSnapshot } from '../config-sync.js';
3
3
  export interface PatchBlock {
4
4
  readonly key: string;
@@ -39,8 +39,8 @@ export declare function configSyncApply(deps: ConfigSyncDeps, payload: unknown):
39
39
  ok: true;
40
40
  written: string[];
41
41
  skipped: string[];
42
- restart: true;
43
42
  } | {
44
43
  ok: false;
45
44
  text: string;
46
45
  };
46
+ export declare function configSyncAction(deps: ConfigSyncDeps): PostResource;
@@ -0,0 +1,18 @@
1
+ import type { DshHostContext } from '@dsh-workbench/shared';
2
+ import type { PostResource, PostWrite } from '@dsh-workbench/shared/host';
3
+ import { type UnitsPlan } from './mount-units.js';
4
+ export interface PostChannelDeps {
5
+ readonly ctx: DshHostContext;
6
+ readonly mounted: ReadonlyMap<string, {
7
+ readonly plan: UnitsPlan;
8
+ }>;
9
+ readonly facts: {
10
+ bump(names: readonly string[]): unknown;
11
+ };
12
+ readonly shellActions: readonly PostResource[];
13
+ }
14
+ export interface PostChannels {
15
+ rebind(wrote: (resource: string, write: PostWrite) => void): void;
16
+ dispose(): void;
17
+ }
18
+ export declare function createPostChannels(deps: PostChannelDeps): PostChannels;
@@ -0,0 +1,45 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { type SeedCommand } from './runtime-seed';
3
+ export declare const SELF_UPDATE_STATES: readonly ["running", "ok", "rolled-back", "failed"];
4
+ export type SelfUpdateState = (typeof SELF_UPDATE_STATES)[number];
5
+ export interface SelfUpdatePaths {
6
+ readonly dir: string;
7
+ readonly script: string;
8
+ readonly marker: string;
9
+ readonly log: string;
10
+ }
11
+ export interface SelfUpdateRequest {
12
+ readonly dshHome: string;
13
+ readonly profile: string;
14
+ readonly pkg: string;
15
+ readonly from: string;
16
+ readonly to: string;
17
+ readonly pinned: boolean;
18
+ }
19
+ export interface SelfUpdateMarker extends SelfUpdateRequest {
20
+ readonly state: SelfUpdateState;
21
+ readonly version: string;
22
+ readonly at: string;
23
+ }
24
+ export interface SelfUpdatePlan {
25
+ readonly paths: SelfUpdatePaths;
26
+ readonly script: string;
27
+ readonly marker: SelfUpdateMarker;
28
+ readonly command: SeedCommand;
29
+ }
30
+ export interface SelfUpdateStart {
31
+ readonly started: boolean;
32
+ readonly paths: SelfUpdatePaths;
33
+ readonly forward: string;
34
+ }
35
+ export declare function runningVersionOf(moduleUrl: string): string | undefined;
36
+ export declare function selfUpdatePaths(dshHome: string): SelfUpdatePaths;
37
+ export declare function recoveryCommand(input: {
38
+ readonly profile: string;
39
+ readonly pkg: string;
40
+ readonly version: string;
41
+ }): string;
42
+ export declare function selfUpdateMarker(request: SelfUpdateRequest, state: SelfUpdateState, version: string, at: string): SelfUpdateMarker;
43
+ export declare function selfUpdateScript(request: SelfUpdateRequest, paths: SelfUpdatePaths): string;
44
+ export declare function planSelfUpdate(request: SelfUpdateRequest, now?: Date): SelfUpdatePlan;
45
+ export declare function startSelfUpdate(request: SelfUpdateRequest, onError: (message: string) => void, spawnImpl?: typeof spawn): SelfUpdateStart;
package/docs/HANDOFF.md CHANGED
@@ -67,7 +67,7 @@ npx @dsh-workbench/core
67
67
 
68
68
  - **要配什么就在 `:3081` 配**,落在 `profiles/workbench/cordis.patch.yml` —— 那是它唯一的那份设置文档;
69
69
  - **想用 web 那边已经配好的行**(模型供应商 / API key 引用 / 搜索源 / MCP)⇒ `:3081` →
70
- `Meta+,` → 「配置同步」→ 选来源 `web` → 勾行 → 同步 → **重启工作台**。
70
+ `Meta+,` → 「配置同步」→ 选来源 `web` → 勾行 → 同步(即生效,不用重启)。
71
71
  它读的是那个 profile 的 `cordis.patch.yml`,按行搬,只搬**显式配过**的行;
72
72
  同 id 两边内容不同时**默认不覆盖**,要覆盖得自己勾;行点名的包没装会当场说出来。
73
73
  - **web 后来改了不会自动过来** —— 再打开这一页,那几行会重新以「差异」标签出现在这一页,拉一次即可。
@@ -188,7 +188,7 @@ manifest;彩排结束、以及「零上传」的失败,都会自动回滚。
188
188
  `pnpm publish` 的人兜底的(外壳那份 = 整仓门禁);不关掉它们,pnpm 会在**彩排与真发各再跑
189
189
  一遍**整门禁 —— 一次发布就是 3 遍门禁 + 3 遍 vitest + 每个包多两轮构建(实测 pnpm 10.32.1:
190
190
  `--dry-run` 一样触发)。来历见
191
- [`.scratch/dsh-ui-alignment/spec.md`](../../../.scratch/dsh-ui-alignment/spec.md) §18.45。
191
+ [`../../../.scratch/dsh-ui-alignment/issues/12-s18-pduiv2.md`](../../../.scratch/dsh-ui-alignment/issues/12-s18-pduiv2.md) §18.45。
192
192
 
193
193
  脚本把这几件事变成**硬失败**(都在任何上传之前):
194
194
 
@@ -208,7 +208,7 @@ manifest;彩排结束、以及「零上传」的失败,都会自动回滚。
208
208
  自己拥有的转发脚本。这条由外壳 `selfcheck` 的判据钉着(门禁里跑)。
209
209
  这类错**本机复现不出来**,只在别人机器上炸
210
210
  (0.1.6 就是这么坏在 `seed-profile.sh: Permission denied` 上的,见
211
- [`.scratch/dsh-ui-alignment/spec.md`](../../../.scratch/dsh-ui-alignment/spec.md) §18.41)。
211
+ [`.scratch/dsh-ui-alignment/issues/53-18-41-permission-denied-pnpm.md`](../../../.scratch/dsh-ui-alignment/issues/53-18-41-permission-denied-pnpm.md) §18.41)。
212
212
 
213
213
  ## 八、版本与兼容(实话)
214
214
 
@@ -60,7 +60,7 @@
60
60
  | 开发(监听改动 + 起工作台) | `pnpm run dev` —— profile 不对会先 `setup`;`:3081` 已在跑就只监听 |
61
61
  | 换端口 | `WORKBENCH_PORT=3091 ./bin/dsh-workbench` |
62
62
  | 改工作台设置 | 在 `:3081` 的 Settings 页改,落在 `profiles/workbench/cordis.patch.yml`。**这是工作台唯一的那份设置文档**(没有别处叠在它上面),所以模型供应商 / 搜索源 / MCP 每一行都改得动 |
63
- | 用 web 那边已经配好的行 | `:3081` → `Meta+,` → 「配置同步」→ 选来源(默认 `web`)→ 勾行 → 同步 → **重启工作台**。只搬你在那个 profile 里**显式配过**的行;行点名的包没装时页面会说出来(ADR-0044) |
63
+ | 用 web 那边已经配好的行 | `:3081` → `Meta+,` → 「配置同步」→ 选来源(默认 `web`)→ 勾行 → 同步(`dsh-hmr` 会重组,**不用重启**)。只搬你在那个 profile 里**显式配过**的行;行点名的包没装时页面会说出来(ADR-0044) |
64
64
  | 改 web 设置 | 在 `:3080` 改,落在 `profiles/web/cordis.patch.yml`。工作台**不会**再自动跟着变 —— 要就跟上面那条同步一次;不想跟就不动 |
65
65
  | 重建 profile | `mv ~/.dsh/profiles/workbench ~/.dsh/profiles/workbench.bak` 后重跑 seed |
66
66
  | **别在设置里关掉工作台** | Settings → 插件里 `@dsh-workbench/core` 那个开关**不要关**。关掉=把它从 profile 的 `bundles` 里移除 → 我们那份 bundle patch(含"禁用出厂左栏")**整份不再应用** → 页面退化成**原版 dsh web**(左栏回来、右侧面板消失),**数据一个都不丢**。恢复:插件页把开关打开(左栏的「插件」入口也在;进不去设置就按 `Meta+,`)。实测过这条降级路径,不是推断 |
@@ -86,7 +86,8 @@ WORKBENCH_PORT=3091 pnpm run dev # 换端口(监听与实例同一个端口
86
86
  | 浏览器 | Vite 重建 `client.js` → `dsh-client-hmr` 每 500ms stat-poll 到 mtime/大小变化 → `/plugins/events` 发 `rebuilt` → 浏览器作废旧模块、预取新包、换 fiber | 插件内的 React 状态会丢;会话 / 工作区 / 连接状态保留 |
87
87
  | 宿主 | Vite 重建 `dist/host/index.js` → `dsh-hmr` 按模块 realpath 匹配并重载插件 | 插件重建,进程与其它插件不受影响 |
88
88
 
89
- **只有这两种改动要重启**:`cordis.patch.yml`(组合层)与 `package.json` 的 `dsh.profile.bundles`。
89
+ `cordis.patch.yml`(组合层)与 `package.json` 的 `dsh.profile.bundles` **也在监听里**:`dsh-hmr` 变了就 `recomposeProfile`,
90
+ 不用重启(它报 `restartReason` 时才按提示重启)。仍然要重启的是改 `vite.config.ts`。
90
91
 
91
92
  宿主侧监听为什么要环境变量:出厂 `hmr` 的 `root` 默认是 `[]`(不监听),
92
93
  而相对路径以 profile 目录为基准 —— 我们的包在 profile 里只是一个指向本仓库的符号链接,
@@ -128,6 +129,35 @@ WORKBENCH_PORT=3091 pnpm run dev # 换端口(监听与实例同一个端口
128
129
  | opencli **daemon** | ❌ **整机一个,隔离不了** | 端口 **19825 被扩展写死**(`background.js` 里 `DAEMON_PORT = 19825`;CLI 侧 `OPENCLI_DAEMON_PORT` 设非默认值直接报错)。所以本机的全局 opencli 与工作台的隔离运行时**抢同一个 daemon**:两边版本不一致时,谁的命令后跑谁就把 daemon 重启成自己那版 |
129
130
  | profile 的 pnpm 源(私有 scope) | ⚠️ **由工作台写进去** | `<profile>/.npmrc` 里的 `@scope:registry=…`,前面带一行 `# dsh-workbench 写的…` 标记。**为什么在工作台这一侧写**:dsh 的卸载没有源参数(`runPnpm(['remove', name])`),pnpm 只能从 profile 的 `.npmrc` 链知道源;不写它,装私有源的模块能成、卸不了。规则与边界见 [ADR-0030](../../../docs/adr/0030-private-registries-live-in-the-profile-pnpm-config.md):只加不改、只写 scope 行、不碰 `~/.npmrc`。**删了会怎样**:下次从这个源装会再写回来;已经装着的那种,点卸载时撞 404 会自愈补回再重试一次 |
130
131
 
132
+ ## 工作台自己怎么升级、出事怎么救
133
+
134
+ 工作台那一行的「更新」**不会把自己摘掉**(见 [ADR-0045](../../docs/adr/0045-workbench-self-update-runs-outside-the-workbench.md)):
135
+ 点下去之后工作台把现场写到 `$DSH_HOME/workbench/self-update.sh|json|log`,然后起一个**独立进程**去做
136
+ `dsh plugin --profile <profile> add @dsh-workbench/core@<版本>` —— 就地换版本,一行都不摘,
137
+ 所以它换的就是工作台自己也没关系(换的是外头那个进程)。
138
+
139
+ - 成功:`self-update.json` 里 `state: "ok"`,`version` 是新版;**重启一次 dsh** 才用得上(面板会这么说)。
140
+ - 换版没成:脚本自己把旧版本装回去,`state: "rolled-back"` —— 工作台不会因此消失。
141
+ - 回滚也没成:`state: "failed"`,日志最后一行给出救法。终端敲这一条即可:
142
+ `dsh plugin --profile <profile> add @dsh-workbench/core@<版本>`(`<版本>` 取源上最新,或 `latest`)。
143
+
144
+ 两种安装形态的差别:
145
+
146
+ | 怎么装的 | profile 里那一条 | 市场自更新 |
147
+ |---|---|---|
148
+ | `npm i -g @dsh-workbench/core` 然后 `dsh-workbench setup` | `@dsh-workbench/core@^x.y.z`(源上) | 能(就是上面那一条) |
149
+ | 仓库副本(`pnpm run dev` / `link:` 本目录) | `link:…` | 拒:不把开发副本换成源上那份 |
150
+
151
+ 全局那一份**启动器**不会跟着升级(它只是启动器:真正加载的是 profile 里那份)。换 profile 或重建之前,
152
+ 先 `npm i -g @dsh-workbench/core@latest`;已经在跑的 profile 走市场那一颗「更新」。
153
+
154
+ ### 起不来:`failed to parse overlay …/cordis.patch.yml`(YAMLException: end of the stream …)
155
+
156
+ 早期版本 `seed-profile.sh` 会把隔离层直接接到 dsh 新模板的 patch 后面,而那份模板的内容是「横幅注释 +
157
+ 一个空数组 `[]`」—— 空数组本身是一个**完整的 YAML 文档**,后面再接序列就解析不了,profile 一条都组不起来。
158
+ 现在的脚本追加前会先把那行单独的 `[]` 让位(`selfcheck` 有判据钉着)。如果你手上那份 profile 是旧脚本建出来的,
159
+ 手工把 `cordis.patch.yml` 里**单独一行**的 `[]` 删掉再起即可(其余内容不用动)。
160
+
131
161
  ## 排障
132
162
 
133
163
  | 症状 | 原因 |
@@ -136,7 +166,7 @@ WORKBENCH_PORT=3091 pnpm run dev # 换端口(监听与实例同一个端口
136
166
  | 面板一直空着、`/workbench/api/issues` 返回 `coding-not-logged-in` | opencli 登录态失效:在终端跑 `opencli coding team-current` 完成登录 |
137
167
  | 面板显示"这个项目不是 Coding 仓库" | 该工作区的 git `origin` 不指向 `e.coding.net`(或没有 origin) |
138
168
  | 打开面板/按刷新要等 5–8 秒 | 正常:opencli 每次调用有**固定约 5 秒**开销(起浏览器会话;接口本身只要 ~260ms)。**没有缓存**——数据一律实时,列表 = 缺陷 + 状态目录两次调用(限并发 2,实测约 7 秒) |
139
- | 改了源码没变化 | 忘了 `pnpm run build`,或没重启进程(宿主缓存已加载的包) |
169
+ | 改了源码没变化 | 忘了 `pnpm run build`,或没刷新页面;改过 `vite.config.ts` 后没重启(正在跑的 watch 用的还是旧配置) |
140
170
  | 市场页点「卸载」报 `没成功:… ERR_PNPM_FETCH_404 GET https://registry.npmjs.org/@你的私有 scope/…` 之类 | 这个 profile 里的私有包不在 pnpm 默认源上,而 dsh 的卸载**不带源**。工作台会自己补上 `<profile>/.npmrc` 的 `@scope:registry=` 再重试一次([ADR-0030](../../../docs/adr/0030-private-registries-live-in-the-profile-pnpm-config.md));**还失败**说明那个包没有 scope(一行 scope 行表达不了),这时才需要人/agent 上手:`dsh plugin --profile <p> remove '<包名>' --config.registry=<那个源>`(`pnpm remove` 不认 `--registry=`),或者给 profile 自己配一行默认 `registry=` |
141
171
  | 体检那屏「检测浏览器通道 · 没连上」,可面板照样能用 | 本机全局 opencli 与隔离运行时版本不一致 ⇒ daemon 被顶成对方那版(`Stale daemon detected: daemon vX != CLI vY`)。下一条浏览器命令会自动把它重启回来,体检也会复检后改回「已连接」([ADR-0024](../../../docs/adr/0024-warm-the-sites-before-the-panel.md) §3.2.1)。**要彻底不抖**:让两边同版本 —— 把 `OPENCLI_PIN`(`src/shared/config/constants.ts`)对齐到 `opencli --version`,再 `npm i --prefix ~/.dsh/workbench/opencli/toolchain @jackwener/opencli@<那个版本> --cache ~/.dsh/workbench/opencli/npm-cache` |
142
172
  | Settings 页报 `overridden by a home patch` | 想在 Settings 里改的行被 `$DSH_HOME/cordis.patch.yml` 或 `--patch` 覆盖了 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/core",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "个人工作台:Agent 工作台外壳(左栏 + 常驻对话 + 自研右侧数据面板)。",
5
5
  "type": "module",
6
6
  "icon": "./icon.svg",
@@ -42,7 +42,7 @@
42
42
  "title": "Workbench",
43
43
  "description": "个人工作台"
44
44
  },
45
- "//scripts": "包内不跑 check:docs —— 那份文档管理块住在仓库根的 docs/ARCHITECTURE.md,由根 `pnpm run check` 用 --config 指到本包的 arch.config.mjs 校验。prepublishOnly 走 -w 跑根那一整套。",
45
+ "//scripts": "包内不跑 check:docs —— 那份文档管理块住在仓库根的 docs/ARCHITECTURE.md,由根 `pnpm run check` 用 --config 指到本包的 arch.config.mjs 校验。check 与 prepublishOnly 都走 -w 跑根那一整套:包内自己串一份会漏掉 lint(react-hooks 那几条规则只在这里跑)、check:docs 与模块工具包的十一条判据 —— 2026-10-16 实测踩到(渲染期写 ref,包内全绿、根上一跑就红)。",
46
46
  "//dsh.bundle.patch": "一份 preset 就是一个补丁层:它 insert 一行 @deepseek-ai/dsh-agent-preset,config.plugins 是整表插件清单。diy.patch.yml 是**生成的**(scripts/gen-diy-preset.mjs,从 dsh 出厂那份 standard.patch.yml 派生),selfcheck 有一条判据盯着它有没有过期。",
47
47
  "dsh": {
48
48
  "bundle": {
@@ -87,7 +87,7 @@
87
87
  "access": "public"
88
88
  },
89
89
  "dependencies": {
90
- "@dsh-workbench/shared": "0.2.6"
90
+ "@dsh-workbench/shared": "0.2.7"
91
91
  },
92
92
  "scripts": {
93
93
  "build": "node scripts/build.mjs && node scripts/build-types.mjs",
@@ -97,7 +97,7 @@
97
97
  "arch": "arch-guard",
98
98
  "selfcheck": "node scripts/selfcheck.mjs",
99
99
  "check:runtimes": "node scripts/check-runtimes.mjs",
100
- "check": "npm run typecheck && npm run coverage && npm run arch && npm run check:runtimes && npm run selfcheck",
100
+ "check": "pnpm -w run check",
101
101
  "test": "vitest run",
102
102
  "coverage": "vitest run --coverage",
103
103
  "dev": "node scripts/dev.mjs",
@@ -87,6 +87,15 @@ console.log("→ 已继承 web 的 bundle 列表: " + merged.join(", "));
87
87
  if grep -q 'id: session-persistence-jsonl' "$DST_PATCH"; then
88
88
  echo "→ 隔离层已存在,跳过"
89
89
  else
90
+ # dsh 生成的 profile patch 是「三行横幅注释 + 一个空数组 []」。空数组本身是一个**完整的
91
+ # YAML 文档**,往后直接接一个序列,解析就会在最外层报
92
+ # "end of the stream or a document separator is expected",profile 直接起不来
93
+ # (实测 dsh 0.2.1-alpha.3:setup 出来的 profile 报这条,去掉那行 [] 才起)。
94
+ # 所以追加之前先把那行单独的 [] 拿掉;本来就没有(或已经是序列)时这一步什么都不做。
95
+ if grep -qx '\[\]' "$DST_PATCH"; then
96
+ grep -vx '\[\]' "$DST_PATCH" >"$DST_PATCH.tmp" && mv "$DST_PATCH.tmp" "$DST_PATCH"
97
+ echo "→ 模板 patch 里的空数组 [] 已让位(否则接不上隔离层)"
98
+ fi
90
99
  printf '\n' >>"$DST_PATCH"
91
100
  cat "$ISOLATION" >>"$DST_PATCH"
92
101
  echo "→ 已追加隔离层(sessions / storages / attachments 分家)"