@dsh-workbench/core 0.3.8 → 0.3.9

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 CHANGED
@@ -23,7 +23,7 @@ dsh web (:3080) dsh workbench (:3081)
23
23
 
24
24
  | 要有 | 为什么 | 怎么有 |
25
25
  | --- | --- | --- |
26
- | `dsh` | 本体。本包在 `0.1.7-rc.2` 上验过 | `npm i -g @deepseek-ai/dsh` |
26
+ | `dsh` | 本体。本包在 `0.2.1-rc.2` 上验过;**更旧的会被拒绝**(`engines.dsh`,旧到不认识它的 dsh 由启动器当场拒绝并给升级命令)—— DIY 模式是从那一代 dsh 的 preset 清单派生的,旧一代它会整个不出现且不报错 | `npm i -g @deepseek-ai/dsh@latest` |
27
27
  | `node` + `pnpm` | dsh 的 `plugin` 子命令是**转发给 pnpm** 的;没有 pnpm 会直接报 `dsh: pnpm was not found` | 各自装 |
28
28
  | 跑过一次 `dsh web` | `setup` 拿 `~/.dsh/profiles/web` 当模板(**bundle 清单**:这台机器上装了哪些 bundle),也是「配置同步」的默认来源。没有它会**明确报错**,不会静默建出半个 profile | `dsh web`,起来就行,然后 Ctrl-C |
29
29
 
@@ -111,6 +111,7 @@ npx @dsh-workbench/core # ② 起工作台,默认 :3081
111
111
  | 一切正常,但左栏是出厂样子、右边没有面板 | 工作台插件在 Settings 里被关了(见上一条 ⚠️)。**重启之后**启动器不会再让你拿到这种实例:它读到 `dsh.profile.bundles` 里没有它就直接拒绝启动,并打印 `./bin/dsh-workbench setup` |
112
112
  | 模型列表里只有出厂的几个、没有自己配的供应商 | 新装的 profile 不带设置(ADR-0044):设置 → 「配置同步」→ 选来源 → 勾 `llm-` 那几行 → 同步(即生效)|
113
113
  | 「配置同步」里某一行说 `@x 还没装` | 那一行点名的包不在这个 profile 的 `node_modules` 里:`dsh plugin --profile workbench add @x`(或在模块市场装),(装完 dsh 会自行重组;它报 `restartReason` 时才重启)|
114
+ | 左栏没有「工作台DIY」那一项、模式下拉里也没有「工作台 DIY」 | 多半是 dsh 太旧(DIY 模式是从 **dsh 0.2.1-rc.2** 的 preset 清单派生的;旧一代那个 preset 挂不起来,而 dsh 的模式选择器**只列挂得起来的** ⇒ 整个不见且不报错)。**启动体检会把它摆出来并停在那一页**(「检查 DIY 模式 · 挂不起来」+ 缺哪几行 + 升级命令 + 一颗「先进入工作台」);启动器更早一步:dsh 太旧就直接拒绝启动/安装并打印 `npm i -g @deepseek-ai/dsh@latest`,装完**重启工作台**即可。想自己核对是哪一行挂不起来:`dsh` 的 设置 → 「Agent presets」里那一行会带红标与原因 |
114
115
 
115
116
  排障全表在仓库的 [`docs/OPERATIONS.md`](https://github.com/code-farmer-i/dsh-workbench/blob/main/packages/workbench/docs/OPERATIONS.md);
116
117
  把工作台装到别人机器上的前置清单见 [`docs/HANDOFF.md`](https://github.com/code-farmer-i/dsh-workbench/blob/main/packages/workbench/docs/HANDOFF.md)。
package/bin/dsh-workbench CHANGED
@@ -15,7 +15,9 @@
15
15
  # · **仓库副本**(这个包落在一个 git 工作树里):开发用。bundle 以 link 方式装本目录,
16
16
  # 改源码立刻生效(配合 `pnpm run dev`)。
17
17
  # · **安装包**(从内网源下下来的):`setup` 把 **registry 上那个包** 装进 profile。
18
- # 判据是「git 顶层在不在」—— `npm pack` 的产物里不含 .git(见 package.json 的 files)。
18
+ # 判据是「**这个包的 package.json 被那个 git 顶层跟踪**」—— `npm pack` 的产物里不含 .git
19
+ # (见 package.json 的 files);只问"顶层在不在"不够:装出来的那份完全可能落在某个仓库里
20
+ # (`$HOME` 是 dotfiles 仓库、而 npm 前缀就在 `$HOME` 下,实测踩过 —— 症状见下面 GIT_TOP 那段)。
19
21
  # ⚠️ 不能判 `$REPO_DIR/.git`:workspace 之后 .git 在**仓库根**,不在这个包里(实测踩过,
20
22
  # 判错的症状是"仓库副本却去源上装",改源码不再生效,而且不报错)。
21
23
 
@@ -38,12 +40,76 @@ while [ -L "$SELF" ]; do
38
40
  done
39
41
  REPO_DIR="$(cd "$(dirname "$SELF")/.." && pwd -P)"
40
42
  GIT_TOP="$(git -C "$REPO_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
43
+ # `rev-parse --show-toplevel` 只说明"**某个祖先目录**是个仓库"—— 装出来的那份完全可能刚好落在
44
+ # 某个仓库里:最常见的是 `$HOME` 本身就是 dotfiles 仓库,而 npm 的前缀(`~/.nvm/...`、
45
+ # `~/.npm-global`…)就在 `$HOME` 下。那会把"装出来的那份"当成仓库副本,以 link 装进 profile:
46
+ # 市场里工作台那一行会说「这份是本仓正在开发的那一份」,而 `npm i -g …@latest` 之后 profile
47
+ # 指向的东西还会悄悄变。所以还得证明**这份 package.json 本身被那个仓库跟踪**(仓库副本才有的事;
48
+ # 与 seed-workshop.sh 那条"工作树里真有 packages/workbench + packages/module-tools"同旨)。
49
+ if [ -n "$GIT_TOP" ] && ! git -C "$REPO_DIR" ls-files --error-unmatch package.json >/dev/null 2>&1; then
50
+ GIT_TOP=""
51
+ fi
41
52
 
42
53
  if ! command -v dsh >/dev/null 2>&1; then
43
54
  echo "dsh-workbench: PATH 上没有 dsh。先装:npm i -g @deepseek-ai/dsh" >&2
44
55
  exit 1
45
56
  fi
46
57
 
58
+ # ── 硬门禁:dsh 太旧就别起(DIY 模式会整个不见,而且一句错都不报)────────────
59
+ #
60
+ # DIY 模式就是 `preset/diy.patch.yml` 那一行 agent preset,而它是**从 dsh 出厂那份
61
+ # `standard.patch.yml` 派生的**(scripts/gen-diy-preset.mjs)—— 所以它跟的是**派生那一刻的
62
+ # dsh**。dsh 旧到那份清单里有几个包它根本没有时(实测 0.2.0-rc.2 缺
63
+ # `@deepseek-ai/dsh-tool-schedule`),preset 挂不起来;dsh 的模式选择器**只列挂得起来的**
64
+ # (healthy presets only),于是「工作台 DIY」在用户面前整个消失,界面与终端都不吭声。
65
+ # 同一代 dsh 0.2.0 的 `uiWorkspace.startSession()` 也**不收** `agentPreset`(0.2.1-alpha.3 才收),
66
+ # 所以就算 preset 挂上了,「造一个模块」也落不到 DIY 模式 —— 这一档没有"少一把工具"的降级空间。
67
+ #
68
+ # 为什么门禁写在启动器、而不只写 package.json 的 `engines.dsh`:
69
+ # engines.dsh 只有 **0.2.1-alpha.4 起**的 dsh 才读(见 HANDOFF §八),而这台机器上那份 dsh
70
+ # 恰恰可能就是旧到不认识的(0.2.0-rc.2 只读 peerDependencies,而 peer 不能写 ——
71
+ # pnpm 的 auto-install-peers 会把整套 dsh 拖进 profile)。所以旧 dsh 由这里兜。
72
+ #
73
+ # 下限只有一处真相:package.json 的 engines.dsh。这里只读它,别在这儿另写一个版本号。
74
+ DSH_FLOOR="$(WB_MANIFEST="$REPO_DIR/package.json" node -e 'const p = require(process.env.WB_MANIFEST); const e = p.engines ?? {}; process.stdout.write(typeof e.dsh === "string" ? e.dsh : "");' 2>/dev/null || true)"
75
+ DSH_VERSION="$(dsh --version 2>/dev/null | tr -d '[:space:]' | sed 's/^v//' || true)"
76
+
77
+ if [ -n "$DSH_FLOOR" ] && [ -n "$DSH_VERSION" ]; then
78
+ # `>=X.Y.Z[-pre]` 一档:与 dsh 自己那条预检同一口径(semver + includePrerelease)。
79
+ # 认不出的下限形状就不拦 —— 这条是给用户的诊断门,权威仍是 dsh 自己的预检。
80
+ DSH_FLOOR="$DSH_FLOOR" DSH_VERSION="$DSH_VERSION" node -e '
81
+ const floor = /^>=\s*(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec((process.env.DSH_FLOOR ?? "").trim());
82
+ const got = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec((process.env.DSH_VERSION ?? "").trim());
83
+ if (floor === null || got === null) process.exit(0);
84
+ const want = { nums: [+floor[1], +floor[2], +floor[3]], pre: floor[4] === undefined ? [] : floor[4].split(".") };
85
+ const have = { nums: [+got[1], +got[2], +got[3]], pre: got[4] === undefined ? [] : got[4].split(".") };
86
+ for (let i = 0; i < 3; i += 1) {
87
+ if (have.nums[i] !== want.nums[i]) process.exit(have.nums[i] > want.nums[i] ? 0 : 1);
88
+ }
89
+ if (have.pre.length === 0) process.exit(0);
90
+ if (want.pre.length === 0) process.exit(1);
91
+ for (let i = 0; i < Math.max(have.pre.length, want.pre.length); i += 1) {
92
+ const a = have.pre[i];
93
+ const b = want.pre[i];
94
+ if (a === undefined) process.exit(1);
95
+ if (b === undefined) process.exit(0);
96
+ const an = /^\d+$/.test(a);
97
+ const bn = /^\d+$/.test(b);
98
+ if (an && bn) { if (+a !== +b) process.exit(+a > +b ? 0 : 1); continue; }
99
+ if (an !== bn) process.exit(an ? 1 : 0);
100
+ if (a !== b) process.exit(a > b ? 0 : 1);
101
+ }
102
+ process.exit(0);
103
+ ' || DSH_FLOOR_GUARD=$?
104
+ if [ "${DSH_FLOOR_GUARD:-0}" -eq 1 ]; then
105
+ echo "dsh-workbench: 这台机器上的 dsh 是 ${DSH_VERSION},这份外壳要 ${DSH_FLOOR}。" >&2
106
+ echo " 低于它时 DIY 模式(会话里的「工作台 DIY」)挂不起来,而 dsh 的模式选择器只列挂得起来的" >&2
107
+ echo " —— 它会**整个不出现**,不报错。所以这里直接拒绝启动。" >&2
108
+ echo " 升级 dsh(再重启一次工作台): npm i -g @deepseek-ai/dsh@latest" >&2
109
+ exit 1
110
+ fi
111
+ fi
112
+
47
113
  pkg_version() {
48
114
  node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo "0.0.0"
49
115
  }
package/client.js CHANGED
@@ -2781,7 +2781,8 @@ window.__ModuleLoader__.load({
2781
2781
  "env.stepFailed.shell": "执行通道不可用",
2782
2782
  "env.bridge.ok": "已连接",
2783
2783
  "env.bridge.short": "没连上",
2784
- "env.bridge.why": "面板的数据要走你浏览器里的登录态:这个通道没连上,模块就取不到数据(不拦路,但先修好它)。",
2784
+ "env.bridge.why": "面板的数据要走你浏览器里的登录态:这个通道没连上,模块就取不到数据(不拦路:修好它,或者先进入工作台)。",
2785
+ "env.bridge.hold": "浏览器通道还没连上 —— 按下面修好后点「重试」,也可以先进工作台。",
2785
2786
  "env.bridge.daemon": "后台服务没起来:正常它会自己起,先在终端跑下面这条命令。",
2786
2787
  "env.bridge.extension": "浏览器扩展没连上:在 Chrome 应用商店装一次就好(装完回来点「重试」)。已经装过的话,先跑下面这条命令。",
2787
2788
  "env.bridge.extensionLink": "去 Chrome 应用商店装扩展",
@@ -2799,11 +2800,21 @@ window.__ModuleLoader__.load({
2799
2800
  "env.fix.network": "网络不通:检查网络或代理设置后重试。",
2800
2801
  "env.fix.details": "详细输出(排障用)",
2801
2802
  "env.retry": "重试",
2803
+ "env.bridge.continue": "先进入工作台",
2804
+ "env.diy.ok": "可用",
2805
+ "env.diy.short": "挂不起来",
2806
+ "env.diy.hold": "DIY 模式挂不起来 —— 升级 dsh 后点「重试」,也可以先进工作台。",
2807
+ "env.diy.why": "「工作台 DIY」是 dsh 的一个 agent preset,而它是照这份外壳验过的那一代 dsh 生成的:这台机器上的 dsh 比它旧,其中几行 dsh 根本没有,于是这个模式整个挂不起来。dsh 的模式选择器只列挂得起来的,进去之后它会整个不出现 —— 面板与其它功能不受影响。",
2808
+ "env.diy.rows": "这一代 dsh 缺这几行:{rows}",
2809
+ "env.diy.noRow": "体检没在 dsh 的 preset 名单里看到「工作台 DIY」—— 多半是这份 bundle 的 preset 补丁没生效。",
2810
+ "env.diy.step": "升级 dsh,然后重启一次工作台:",
2811
+ "env.diy.continue": "先进入工作台",
2802
2812
  "env.copyCommand": "复制命令",
2803
2813
  "env.copied": "已复制",
2804
2814
  "env.step.opencli": "检测工作台的 opencli",
2805
2815
  "env.step.codingPlugin": "检测 coding 插件",
2806
2816
  "env.step.bridge": "检测浏览器通道",
2817
+ "env.step.diy": "检查 DIY 模式",
2807
2818
  "env.step.warm": "预热站点登录态",
2808
2819
  "env.warm.done": "已预热 {n} 个站点",
2809
2820
  "env.warm.short": "这次没热上 —— 不影响用,面板第一次读取会自己补",
@@ -3225,7 +3236,8 @@ window.__ModuleLoader__.load({
3225
3236
  "env.stepFailed.shell": "no shell channel",
3226
3237
  "env.bridge.ok": "Connected",
3227
3238
  "env.bridge.short": "Not connected",
3228
- "env.bridge.why": "Panel data goes through the login state in your browser: with this channel down, modules cannot fetch data. It does not block startup, but fix it first.",
3239
+ "env.bridge.why": "Panel data goes through the login state in your browser: with this channel down, modules cannot fetch data. It does not block startup: fix it, or enter the workbench first.",
3240
+ "env.bridge.hold": "The browser bridge is not connected yet — fix it below and hit Retry, or enter the workbench first.",
3229
3241
  "env.bridge.daemon": "The background service is not running. It usually starts by itself — run the command below in your terminal first.",
3230
3242
  "env.bridge.extension": "The browser extension is not connected. Install it once from the Chrome Web Store (then hit Retry here). If it is already installed, run the command below first.",
3231
3243
  "env.bridge.extensionLink": "Get the extension in the Chrome Web Store",
@@ -3243,11 +3255,21 @@ window.__ModuleLoader__.load({
3243
3255
  "env.fix.network": "Network unreachable: check your network or proxy, then retry.",
3244
3256
  "env.fix.details": "Details (for troubleshooting)",
3245
3257
  "env.retry": "Retry",
3258
+ "env.bridge.continue": "Enter the workbench anyway",
3259
+ "env.diy.ok": "Available",
3260
+ "env.diy.short": "Cannot load",
3261
+ "env.diy.hold": "DIY mode cannot load — upgrade dsh, then hit Retry; or enter the workbench first.",
3262
+ "env.diy.why": "“Workbench DIY” is a dsh agent preset, and it is generated from the dsh generation this shell was verified against. The dsh on this machine is older and does not ship some of those rows, so the whole preset fails to mount. dsh lists only the presets that mount, so the mode will simply not appear — panels and everything else are unaffected.",
3263
+ "env.diy.rows": "This dsh is missing these rows: {rows}",
3264
+ "env.diy.noRow": "The health check did not find “Workbench DIY” in dsh’s preset roster — most likely this bundle’s preset patch did not apply.",
3265
+ "env.diy.step": "Upgrade dsh, then restart the workbench:",
3266
+ "env.diy.continue": "Enter the workbench anyway",
3246
3267
  "env.copyCommand": "Copy command",
3247
3268
  "env.copied": "Copied",
3248
3269
  "env.step.opencli": "Checking the workbench opencli",
3249
3270
  "env.step.codingPlugin": "Checking the coding plugin",
3250
3271
  "env.step.bridge": "Checking the browser bridge",
3272
+ "env.step.diy": "Checking DIY mode",
3251
3273
  "env.step.warm": "Warming up site sessions",
3252
3274
  "env.warm.done": "{n} site(s) warmed up",
3253
3275
  "env.warm.short": "Not warmed this time — harmless; the panel heals on first read",
@@ -7799,6 +7821,10 @@ window.__ModuleLoader__.load({
7799
7821
  if (step.status === "ok") return t("env.bridge.ok");
7800
7822
  if (step.status === "warn") return t("env.bridge.short");
7801
7823
  }
7824
+ if (step.id === "diy") {
7825
+ if (step.status === "ok") return t("env.diy.ok");
7826
+ if (step.status === "warn") return t("env.diy.short");
7827
+ }
7802
7828
  if (step.id === "warm") {
7803
7829
  if (step.status === "ok") return t("env.warm.done", { n: step.detail ?? "0" });
7804
7830
  if (step.status === "warn") return t("env.warm.short");
@@ -7894,6 +7920,7 @@ window.__ModuleLoader__.load({
7894
7920
  function stepLabel$1(id, t) {
7895
7921
  if (id === "opencli") return t("env.step.opencli");
7896
7922
  if (id === "warm") return t("env.step.warm");
7923
+ if (id === "diy") return t("env.step.diy");
7897
7924
  return t("env.step.bridge");
7898
7925
  }
7899
7926
  function stepMark(step) {
@@ -7921,6 +7948,18 @@ window.__ModuleLoader__.load({
7921
7948
  }
7922
7949
  };
7923
7950
  }
7951
+ /**
7952
+ * 拿到这一拍之后门该怎么办:`close` 撤掉放行、`hold` 停在原地等用户动手、`wait` 接着轮询。
7953
+ * 桥接坏了(`bridgeIssues` 非空)与 DIY 模式挂不起来(`diyGap` 非空)都是**不拦路**的,
7954
+ * 但那块「怎么解决」得先真的画出来 —— 停在原地 + 一颗「先进入工作台」才叫不拦路,
7955
+ * 就绪即撤等于把那块提示连同修法一起丢掉,用户进去只会发现「工作台 DIY」整个不在。
7956
+ */
7957
+ function gateActionOf(snapshot) {
7958
+ if (snapshot.phase === "failed") return "hold";
7959
+ if (snapshot.phase !== "ready") return "wait";
7960
+ if ((snapshot.bridgeIssues?.length ?? 0) > 0) return "hold";
7961
+ return snapshot.diyGap === void 0 ? "close" : "hold";
7962
+ }
7924
7963
  function commandRow(t) {
7925
7964
  const row = el("div", BootGate_module_default.commandRow);
7926
7965
  const code = el("code", BootGate_module_default.command);
@@ -7993,13 +8032,32 @@ window.__ModuleLoader__.load({
7993
8032
  const bridgeCommand = commandRow(t);
7994
8033
  const bridgeRetry = el("button", BootGate_module_default.button, t("env.retry"));
7995
8034
  bridgeRetry.setAttribute("type", "button");
8035
+ const bridgeSkip = el("button", BootGate_module_default.button, t("env.bridge.continue"));
8036
+ bridgeSkip.setAttribute("type", "button");
7996
8037
  const bridgeButtons = el("div", BootGate_module_default.buttons);
7997
- bridgeButtons.append(bridgeRetry);
8038
+ bridgeButtons.append(bridgeRetry, bridgeSkip);
7998
8039
  const bridgeRaw = el("details", BootGate_module_default.raw);
7999
8040
  const bridgeRawText = el("pre", BootGate_module_default.error);
8000
8041
  bridgeRaw.append(el("summary", BootGate_module_default.rawSummary, t("env.fix.details")), bridgeRawText);
8001
8042
  bridge.append(bridgeTitle, bridgeWhy, bridgeLines, bridgeCommand.row, bridgeButtons, bridgeRaw);
8002
- card.append(title, hint, list, progressRow, actions, bridge);
8043
+ const diy = el("div", BootGate_module_default.bridge);
8044
+ diy.hidden = true;
8045
+ const diyTitle = el("h2", BootGate_module_default.fixTitle, t("env.fix.title"));
8046
+ const diyWhy = el("p", BootGate_module_default.hint, t("env.diy.why"));
8047
+ const diyLines = el("ul", BootGate_module_default.bridgeLines);
8048
+ const diyStep = el("p", BootGate_module_default.hint, t("env.diy.step"));
8049
+ const diyCommand = commandRow(t);
8050
+ const diyRetry = el("button", BootGate_module_default.button, t("env.retry"));
8051
+ diyRetry.setAttribute("type", "button");
8052
+ const diySkip = el("button", BootGate_module_default.button, t("env.diy.continue"));
8053
+ diySkip.setAttribute("type", "button");
8054
+ const diyButtons = el("div", BootGate_module_default.buttons);
8055
+ diyButtons.append(diyRetry, diySkip);
8056
+ const diyRaw = el("details", BootGate_module_default.raw);
8057
+ const diyRawText = el("pre", BootGate_module_default.error);
8058
+ diyRaw.append(el("summary", BootGate_module_default.rawSummary, t("env.fix.details")), diyRawText);
8059
+ diy.append(diyTitle, diyWhy, diyLines, diyStep, diyCommand.row, diyButtons, diyRaw);
8060
+ card.append(title, hint, list, progressRow, actions, bridge, diy);
8003
8061
  overlay.append(card);
8004
8062
  document.body.append(overlay);
8005
8063
  const previousInert = root?.inert ?? false;
@@ -8057,7 +8115,7 @@ window.__ModuleLoader__.load({
8057
8115
  return row;
8058
8116
  }
8059
8117
  function render(snapshot) {
8060
- hint.textContent = snapshot.phase === "installing" ? t("env.installing") : snapshot.phase === "failed" ? t("env.failed") : t("env.checking");
8118
+ hint.textContent = snapshot.phase === "installing" ? t("env.installing") : snapshot.phase === "failed" ? t("env.failed") : gateActionOf(snapshot) === "hold" ? (snapshot.bridgeIssues?.length ?? 0) > 0 ? t("env.bridge.hold") : t("env.diy.hold") : t("env.checking");
8061
8119
  const items = snapshot.steps.map((step) => {
8062
8120
  const row = ensureRow(step);
8063
8121
  row.item.dataset.status = step.status;
@@ -8115,6 +8173,15 @@ window.__ModuleLoader__.load({
8115
8173
  bridgeRawText.textContent = bridgeStep.detail ?? "";
8116
8174
  bridgeRaw.hidden = (bridgeStep.detail ?? "") === "";
8117
8175
  }
8176
+ const gap = snapshot.diyGap;
8177
+ diy.hidden = gap === void 0;
8178
+ if (gap !== void 0) {
8179
+ const lines = gap.rows.length === 0 ? [t("env.diy.noRow")] : [t("env.diy.rows", { rows: gap.rows.join("、") })];
8180
+ diyLines.replaceChildren(...lines.map((text) => el("li", BootGate_module_default.bridgeLine, text)));
8181
+ diyCommand.set(gap.command);
8182
+ diyRawText.textContent = gap.detail;
8183
+ diyRaw.hidden = gap.detail === "";
8184
+ }
8118
8185
  }
8119
8186
  async function poll() {
8120
8187
  if (cancelled) return;
@@ -8126,19 +8193,22 @@ window.__ModuleLoader__.load({
8126
8193
  return;
8127
8194
  }
8128
8195
  render(response);
8129
- if (response.phase === "ready") {
8196
+ const action = gateActionOf(response);
8197
+ if (action === "close") {
8130
8198
  close();
8131
8199
  return;
8132
8200
  }
8133
- if (response.phase === "failed") return;
8201
+ if (action === "hold") return;
8134
8202
  timer = setTimeout(() => void poll(), POLL_MS);
8135
8203
  }
8136
8204
  function retryNow() {
8137
8205
  forcePlan.again();
8138
8206
  actions.hidden = true;
8139
8207
  bridge.hidden = true;
8208
+ diy.hidden = true;
8140
8209
  retry.setAttribute("disabled", "");
8141
8210
  bridgeRetry.setAttribute("disabled", "");
8211
+ diyRetry.setAttribute("disabled", "");
8142
8212
  hint.textContent = t("env.checking");
8143
8213
  if (pinned !== null) {
8144
8214
  render(pinned);
@@ -8148,6 +8218,9 @@ window.__ModuleLoader__.load({
8148
8218
  }
8149
8219
  retry.addEventListener("click", retryNow);
8150
8220
  bridgeRetry.addEventListener("click", retryNow);
8221
+ bridgeSkip.addEventListener("click", close);
8222
+ diyRetry.addEventListener("click", retryNow);
8223
+ diySkip.addEventListener("click", close);
8151
8224
  const search = typeof window === "undefined" ? "" : window.location?.search ?? "";
8152
8225
  if (devSurface.on(search)) {
8153
8226
  const strip = devSurface.gateStrip({ onPick: (snapshot) => {
@@ -2774,7 +2774,8 @@ var dictionaries = {
2774
2774
  "env.stepFailed.shell": "执行通道不可用",
2775
2775
  "env.bridge.ok": "已连接",
2776
2776
  "env.bridge.short": "没连上",
2777
- "env.bridge.why": "面板的数据要走你浏览器里的登录态:这个通道没连上,模块就取不到数据(不拦路,但先修好它)。",
2777
+ "env.bridge.why": "面板的数据要走你浏览器里的登录态:这个通道没连上,模块就取不到数据(不拦路:修好它,或者先进入工作台)。",
2778
+ "env.bridge.hold": "浏览器通道还没连上 —— 按下面修好后点「重试」,也可以先进工作台。",
2778
2779
  "env.bridge.daemon": "后台服务没起来:正常它会自己起,先在终端跑下面这条命令。",
2779
2780
  "env.bridge.extension": "浏览器扩展没连上:在 Chrome 应用商店装一次就好(装完回来点「重试」)。已经装过的话,先跑下面这条命令。",
2780
2781
  "env.bridge.extensionLink": "去 Chrome 应用商店装扩展",
@@ -2792,11 +2793,21 @@ var dictionaries = {
2792
2793
  "env.fix.network": "网络不通:检查网络或代理设置后重试。",
2793
2794
  "env.fix.details": "详细输出(排障用)",
2794
2795
  "env.retry": "重试",
2796
+ "env.bridge.continue": "先进入工作台",
2797
+ "env.diy.ok": "可用",
2798
+ "env.diy.short": "挂不起来",
2799
+ "env.diy.hold": "DIY 模式挂不起来 —— 升级 dsh 后点「重试」,也可以先进工作台。",
2800
+ "env.diy.why": "「工作台 DIY」是 dsh 的一个 agent preset,而它是照这份外壳验过的那一代 dsh 生成的:这台机器上的 dsh 比它旧,其中几行 dsh 根本没有,于是这个模式整个挂不起来。dsh 的模式选择器只列挂得起来的,进去之后它会整个不出现 —— 面板与其它功能不受影响。",
2801
+ "env.diy.rows": "这一代 dsh 缺这几行:{rows}",
2802
+ "env.diy.noRow": "体检没在 dsh 的 preset 名单里看到「工作台 DIY」—— 多半是这份 bundle 的 preset 补丁没生效。",
2803
+ "env.diy.step": "升级 dsh,然后重启一次工作台:",
2804
+ "env.diy.continue": "先进入工作台",
2795
2805
  "env.copyCommand": "复制命令",
2796
2806
  "env.copied": "已复制",
2797
2807
  "env.step.opencli": "检测工作台的 opencli",
2798
2808
  "env.step.codingPlugin": "检测 coding 插件",
2799
2809
  "env.step.bridge": "检测浏览器通道",
2810
+ "env.step.diy": "检查 DIY 模式",
2800
2811
  "env.step.warm": "预热站点登录态",
2801
2812
  "env.warm.done": "已预热 {n} 个站点",
2802
2813
  "env.warm.short": "这次没热上 —— 不影响用,面板第一次读取会自己补",
@@ -3218,7 +3229,8 @@ var dictionaries = {
3218
3229
  "env.stepFailed.shell": "no shell channel",
3219
3230
  "env.bridge.ok": "Connected",
3220
3231
  "env.bridge.short": "Not connected",
3221
- "env.bridge.why": "Panel data goes through the login state in your browser: with this channel down, modules cannot fetch data. It does not block startup, but fix it first.",
3232
+ "env.bridge.why": "Panel data goes through the login state in your browser: with this channel down, modules cannot fetch data. It does not block startup: fix it, or enter the workbench first.",
3233
+ "env.bridge.hold": "The browser bridge is not connected yet — fix it below and hit Retry, or enter the workbench first.",
3222
3234
  "env.bridge.daemon": "The background service is not running. It usually starts by itself — run the command below in your terminal first.",
3223
3235
  "env.bridge.extension": "The browser extension is not connected. Install it once from the Chrome Web Store (then hit Retry here). If it is already installed, run the command below first.",
3224
3236
  "env.bridge.extensionLink": "Get the extension in the Chrome Web Store",
@@ -3236,11 +3248,21 @@ var dictionaries = {
3236
3248
  "env.fix.network": "Network unreachable: check your network or proxy, then retry.",
3237
3249
  "env.fix.details": "Details (for troubleshooting)",
3238
3250
  "env.retry": "Retry",
3251
+ "env.bridge.continue": "Enter the workbench anyway",
3252
+ "env.diy.ok": "Available",
3253
+ "env.diy.short": "Cannot load",
3254
+ "env.diy.hold": "DIY mode cannot load — upgrade dsh, then hit Retry; or enter the workbench first.",
3255
+ "env.diy.why": "“Workbench DIY” is a dsh agent preset, and it is generated from the dsh generation this shell was verified against. The dsh on this machine is older and does not ship some of those rows, so the whole preset fails to mount. dsh lists only the presets that mount, so the mode will simply not appear — panels and everything else are unaffected.",
3256
+ "env.diy.rows": "This dsh is missing these rows: {rows}",
3257
+ "env.diy.noRow": "The health check did not find “Workbench DIY” in dsh’s preset roster — most likely this bundle’s preset patch did not apply.",
3258
+ "env.diy.step": "Upgrade dsh, then restart the workbench:",
3259
+ "env.diy.continue": "Enter the workbench anyway",
3239
3260
  "env.copyCommand": "Copy command",
3240
3261
  "env.copied": "Copied",
3241
3262
  "env.step.opencli": "Checking the workbench opencli",
3242
3263
  "env.step.codingPlugin": "Checking the coding plugin",
3243
3264
  "env.step.bridge": "Checking the browser bridge",
3265
+ "env.step.diy": "Checking DIY mode",
3244
3266
  "env.step.warm": "Warming up site sessions",
3245
3267
  "env.warm.done": "{n} site(s) warmed up",
3246
3268
  "env.warm.short": "Not warmed this time — harmless; the panel heals on first read",
@@ -7792,6 +7814,10 @@ function stepDetailText(step, snapshot, t) {
7792
7814
  if (step.status === "ok") return t("env.bridge.ok");
7793
7815
  if (step.status === "warn") return t("env.bridge.short");
7794
7816
  }
7817
+ if (step.id === "diy") {
7818
+ if (step.status === "ok") return t("env.diy.ok");
7819
+ if (step.status === "warn") return t("env.diy.short");
7820
+ }
7795
7821
  if (step.id === "warm") {
7796
7822
  if (step.status === "ok") return t("env.warm.done", { n: step.detail ?? "0" });
7797
7823
  if (step.status === "warn") return t("env.warm.short");
@@ -7887,6 +7913,7 @@ var GIVE_UP_MS = 12e4;
7887
7913
  function stepLabel$1(id, t) {
7888
7914
  if (id === "opencli") return t("env.step.opencli");
7889
7915
  if (id === "warm") return t("env.step.warm");
7916
+ if (id === "diy") return t("env.step.diy");
7890
7917
  return t("env.step.bridge");
7891
7918
  }
7892
7919
  function stepMark(step) {
@@ -7914,6 +7941,18 @@ function createForcePlan() {
7914
7941
  }
7915
7942
  };
7916
7943
  }
7944
+ /**
7945
+ * 拿到这一拍之后门该怎么办:`close` 撤掉放行、`hold` 停在原地等用户动手、`wait` 接着轮询。
7946
+ * 桥接坏了(`bridgeIssues` 非空)与 DIY 模式挂不起来(`diyGap` 非空)都是**不拦路**的,
7947
+ * 但那块「怎么解决」得先真的画出来 —— 停在原地 + 一颗「先进入工作台」才叫不拦路,
7948
+ * 就绪即撤等于把那块提示连同修法一起丢掉,用户进去只会发现「工作台 DIY」整个不在。
7949
+ */
7950
+ function gateActionOf(snapshot) {
7951
+ if (snapshot.phase === "failed") return "hold";
7952
+ if (snapshot.phase !== "ready") return "wait";
7953
+ if ((snapshot.bridgeIssues?.length ?? 0) > 0) return "hold";
7954
+ return snapshot.diyGap === void 0 ? "close" : "hold";
7955
+ }
7917
7956
  function commandRow(t) {
7918
7957
  const row = el("div", BootGate_module_default.commandRow);
7919
7958
  const code = el("code", BootGate_module_default.command);
@@ -7986,13 +8025,32 @@ function createBootGate(ctx, options = {}) {
7986
8025
  const bridgeCommand = commandRow(t);
7987
8026
  const bridgeRetry = el("button", BootGate_module_default.button, t("env.retry"));
7988
8027
  bridgeRetry.setAttribute("type", "button");
8028
+ const bridgeSkip = el("button", BootGate_module_default.button, t("env.bridge.continue"));
8029
+ bridgeSkip.setAttribute("type", "button");
7989
8030
  const bridgeButtons = el("div", BootGate_module_default.buttons);
7990
- bridgeButtons.append(bridgeRetry);
8031
+ bridgeButtons.append(bridgeRetry, bridgeSkip);
7991
8032
  const bridgeRaw = el("details", BootGate_module_default.raw);
7992
8033
  const bridgeRawText = el("pre", BootGate_module_default.error);
7993
8034
  bridgeRaw.append(el("summary", BootGate_module_default.rawSummary, t("env.fix.details")), bridgeRawText);
7994
8035
  bridge.append(bridgeTitle, bridgeWhy, bridgeLines, bridgeCommand.row, bridgeButtons, bridgeRaw);
7995
- card.append(title, hint, list, progressRow, actions, bridge);
8036
+ const diy = el("div", BootGate_module_default.bridge);
8037
+ diy.hidden = true;
8038
+ const diyTitle = el("h2", BootGate_module_default.fixTitle, t("env.fix.title"));
8039
+ const diyWhy = el("p", BootGate_module_default.hint, t("env.diy.why"));
8040
+ const diyLines = el("ul", BootGate_module_default.bridgeLines);
8041
+ const diyStep = el("p", BootGate_module_default.hint, t("env.diy.step"));
8042
+ const diyCommand = commandRow(t);
8043
+ const diyRetry = el("button", BootGate_module_default.button, t("env.retry"));
8044
+ diyRetry.setAttribute("type", "button");
8045
+ const diySkip = el("button", BootGate_module_default.button, t("env.diy.continue"));
8046
+ diySkip.setAttribute("type", "button");
8047
+ const diyButtons = el("div", BootGate_module_default.buttons);
8048
+ diyButtons.append(diyRetry, diySkip);
8049
+ const diyRaw = el("details", BootGate_module_default.raw);
8050
+ const diyRawText = el("pre", BootGate_module_default.error);
8051
+ diyRaw.append(el("summary", BootGate_module_default.rawSummary, t("env.fix.details")), diyRawText);
8052
+ diy.append(diyTitle, diyWhy, diyLines, diyStep, diyCommand.row, diyButtons, diyRaw);
8053
+ card.append(title, hint, list, progressRow, actions, bridge, diy);
7996
8054
  overlay.append(card);
7997
8055
  document.body.append(overlay);
7998
8056
  const previousInert = root?.inert ?? false;
@@ -8050,7 +8108,7 @@ function createBootGate(ctx, options = {}) {
8050
8108
  return row;
8051
8109
  }
8052
8110
  function render(snapshot) {
8053
- hint.textContent = snapshot.phase === "installing" ? t("env.installing") : snapshot.phase === "failed" ? t("env.failed") : t("env.checking");
8111
+ hint.textContent = snapshot.phase === "installing" ? t("env.installing") : snapshot.phase === "failed" ? t("env.failed") : gateActionOf(snapshot) === "hold" ? (snapshot.bridgeIssues?.length ?? 0) > 0 ? t("env.bridge.hold") : t("env.diy.hold") : t("env.checking");
8054
8112
  const items = snapshot.steps.map((step) => {
8055
8113
  const row = ensureRow(step);
8056
8114
  row.item.dataset.status = step.status;
@@ -8108,6 +8166,15 @@ function createBootGate(ctx, options = {}) {
8108
8166
  bridgeRawText.textContent = bridgeStep.detail ?? "";
8109
8167
  bridgeRaw.hidden = (bridgeStep.detail ?? "") === "";
8110
8168
  }
8169
+ const gap = snapshot.diyGap;
8170
+ diy.hidden = gap === void 0;
8171
+ if (gap !== void 0) {
8172
+ const lines = gap.rows.length === 0 ? [t("env.diy.noRow")] : [t("env.diy.rows", { rows: gap.rows.join("、") })];
8173
+ diyLines.replaceChildren(...lines.map((text) => el("li", BootGate_module_default.bridgeLine, text)));
8174
+ diyCommand.set(gap.command);
8175
+ diyRawText.textContent = gap.detail;
8176
+ diyRaw.hidden = gap.detail === "";
8177
+ }
8111
8178
  }
8112
8179
  async function poll() {
8113
8180
  if (cancelled) return;
@@ -8119,19 +8186,22 @@ function createBootGate(ctx, options = {}) {
8119
8186
  return;
8120
8187
  }
8121
8188
  render(response);
8122
- if (response.phase === "ready") {
8189
+ const action = gateActionOf(response);
8190
+ if (action === "close") {
8123
8191
  close();
8124
8192
  return;
8125
8193
  }
8126
- if (response.phase === "failed") return;
8194
+ if (action === "hold") return;
8127
8195
  timer = setTimeout(() => void poll(), POLL_MS);
8128
8196
  }
8129
8197
  function retryNow() {
8130
8198
  forcePlan.again();
8131
8199
  actions.hidden = true;
8132
8200
  bridge.hidden = true;
8201
+ diy.hidden = true;
8133
8202
  retry.setAttribute("disabled", "");
8134
8203
  bridgeRetry.setAttribute("disabled", "");
8204
+ diyRetry.setAttribute("disabled", "");
8135
8205
  hint.textContent = t("env.checking");
8136
8206
  if (pinned !== null) {
8137
8207
  render(pinned);
@@ -8141,6 +8211,9 @@ function createBootGate(ctx, options = {}) {
8141
8211
  }
8142
8212
  retry.addEventListener("click", retryNow);
8143
8213
  bridgeRetry.addEventListener("click", retryNow);
8214
+ bridgeSkip.addEventListener("click", close);
8215
+ diyRetry.addEventListener("click", retryNow);
8216
+ diySkip.addEventListener("click", close);
8144
8217
  const search = typeof window === "undefined" ? "" : window.location?.search ?? "";
8145
8218
  if (devSurface.on(search)) {
8146
8219
  const strip = devSurface.gateStrip({ onPick: (snapshot) => {
@@ -2777,7 +2777,8 @@ var dictionaries = {
2777
2777
  "env.stepFailed.shell": "执行通道不可用",
2778
2778
  "env.bridge.ok": "已连接",
2779
2779
  "env.bridge.short": "没连上",
2780
- "env.bridge.why": "面板的数据要走你浏览器里的登录态:这个通道没连上,模块就取不到数据(不拦路,但先修好它)。",
2780
+ "env.bridge.why": "面板的数据要走你浏览器里的登录态:这个通道没连上,模块就取不到数据(不拦路:修好它,或者先进入工作台)。",
2781
+ "env.bridge.hold": "浏览器通道还没连上 —— 按下面修好后点「重试」,也可以先进工作台。",
2781
2782
  "env.bridge.daemon": "后台服务没起来:正常它会自己起,先在终端跑下面这条命令。",
2782
2783
  "env.bridge.extension": "浏览器扩展没连上:在 Chrome 应用商店装一次就好(装完回来点「重试」)。已经装过的话,先跑下面这条命令。",
2783
2784
  "env.bridge.extensionLink": "去 Chrome 应用商店装扩展",
@@ -2795,11 +2796,21 @@ var dictionaries = {
2795
2796
  "env.fix.network": "网络不通:检查网络或代理设置后重试。",
2796
2797
  "env.fix.details": "详细输出(排障用)",
2797
2798
  "env.retry": "重试",
2799
+ "env.bridge.continue": "先进入工作台",
2800
+ "env.diy.ok": "可用",
2801
+ "env.diy.short": "挂不起来",
2802
+ "env.diy.hold": "DIY 模式挂不起来 —— 升级 dsh 后点「重试」,也可以先进工作台。",
2803
+ "env.diy.why": "「工作台 DIY」是 dsh 的一个 agent preset,而它是照这份外壳验过的那一代 dsh 生成的:这台机器上的 dsh 比它旧,其中几行 dsh 根本没有,于是这个模式整个挂不起来。dsh 的模式选择器只列挂得起来的,进去之后它会整个不出现 —— 面板与其它功能不受影响。",
2804
+ "env.diy.rows": "这一代 dsh 缺这几行:{rows}",
2805
+ "env.diy.noRow": "体检没在 dsh 的 preset 名单里看到「工作台 DIY」—— 多半是这份 bundle 的 preset 补丁没生效。",
2806
+ "env.diy.step": "升级 dsh,然后重启一次工作台:",
2807
+ "env.diy.continue": "先进入工作台",
2798
2808
  "env.copyCommand": "复制命令",
2799
2809
  "env.copied": "已复制",
2800
2810
  "env.step.opencli": "检测工作台的 opencli",
2801
2811
  "env.step.codingPlugin": "检测 coding 插件",
2802
2812
  "env.step.bridge": "检测浏览器通道",
2813
+ "env.step.diy": "检查 DIY 模式",
2803
2814
  "env.step.warm": "预热站点登录态",
2804
2815
  "env.warm.done": "已预热 {n} 个站点",
2805
2816
  "env.warm.short": "这次没热上 —— 不影响用,面板第一次读取会自己补",
@@ -3221,7 +3232,8 @@ var dictionaries = {
3221
3232
  "env.stepFailed.shell": "no shell channel",
3222
3233
  "env.bridge.ok": "Connected",
3223
3234
  "env.bridge.short": "Not connected",
3224
- "env.bridge.why": "Panel data goes through the login state in your browser: with this channel down, modules cannot fetch data. It does not block startup, but fix it first.",
3235
+ "env.bridge.why": "Panel data goes through the login state in your browser: with this channel down, modules cannot fetch data. It does not block startup: fix it, or enter the workbench first.",
3236
+ "env.bridge.hold": "The browser bridge is not connected yet — fix it below and hit Retry, or enter the workbench first.",
3225
3237
  "env.bridge.daemon": "The background service is not running. It usually starts by itself — run the command below in your terminal first.",
3226
3238
  "env.bridge.extension": "The browser extension is not connected. Install it once from the Chrome Web Store (then hit Retry here). If it is already installed, run the command below first.",
3227
3239
  "env.bridge.extensionLink": "Get the extension in the Chrome Web Store",
@@ -3239,11 +3251,21 @@ var dictionaries = {
3239
3251
  "env.fix.network": "Network unreachable: check your network or proxy, then retry.",
3240
3252
  "env.fix.details": "Details (for troubleshooting)",
3241
3253
  "env.retry": "Retry",
3254
+ "env.bridge.continue": "Enter the workbench anyway",
3255
+ "env.diy.ok": "Available",
3256
+ "env.diy.short": "Cannot load",
3257
+ "env.diy.hold": "DIY mode cannot load — upgrade dsh, then hit Retry; or enter the workbench first.",
3258
+ "env.diy.why": "“Workbench DIY” is a dsh agent preset, and it is generated from the dsh generation this shell was verified against. The dsh on this machine is older and does not ship some of those rows, so the whole preset fails to mount. dsh lists only the presets that mount, so the mode will simply not appear — panels and everything else are unaffected.",
3259
+ "env.diy.rows": "This dsh is missing these rows: {rows}",
3260
+ "env.diy.noRow": "The health check did not find “Workbench DIY” in dsh’s preset roster — most likely this bundle’s preset patch did not apply.",
3261
+ "env.diy.step": "Upgrade dsh, then restart the workbench:",
3262
+ "env.diy.continue": "Enter the workbench anyway",
3242
3263
  "env.copyCommand": "Copy command",
3243
3264
  "env.copied": "Copied",
3244
3265
  "env.step.opencli": "Checking the workbench opencli",
3245
3266
  "env.step.codingPlugin": "Checking the coding plugin",
3246
3267
  "env.step.bridge": "Checking the browser bridge",
3268
+ "env.step.diy": "Checking DIY mode",
3247
3269
  "env.step.warm": "Warming up site sessions",
3248
3270
  "env.warm.done": "{n} site(s) warmed up",
3249
3271
  "env.warm.short": "Not warmed this time — harmless; the panel heals on first read",
@@ -7795,6 +7817,10 @@ function stepDetailText(step, snapshot, t) {
7795
7817
  if (step.status === "ok") return t("env.bridge.ok");
7796
7818
  if (step.status === "warn") return t("env.bridge.short");
7797
7819
  }
7820
+ if (step.id === "diy") {
7821
+ if (step.status === "ok") return t("env.diy.ok");
7822
+ if (step.status === "warn") return t("env.diy.short");
7823
+ }
7798
7824
  if (step.id === "warm") {
7799
7825
  if (step.status === "ok") return t("env.warm.done", { n: step.detail ?? "0" });
7800
7826
  if (step.status === "warn") return t("env.warm.short");
@@ -7890,6 +7916,7 @@ var GIVE_UP_MS = 12e4;
7890
7916
  function stepLabel$1(id, t) {
7891
7917
  if (id === "opencli") return t("env.step.opencli");
7892
7918
  if (id === "warm") return t("env.step.warm");
7919
+ if (id === "diy") return t("env.step.diy");
7893
7920
  return t("env.step.bridge");
7894
7921
  }
7895
7922
  function stepMark(step) {
@@ -7917,6 +7944,18 @@ function createForcePlan() {
7917
7944
  }
7918
7945
  };
7919
7946
  }
7947
+ /**
7948
+ * 拿到这一拍之后门该怎么办:`close` 撤掉放行、`hold` 停在原地等用户动手、`wait` 接着轮询。
7949
+ * 桥接坏了(`bridgeIssues` 非空)与 DIY 模式挂不起来(`diyGap` 非空)都是**不拦路**的,
7950
+ * 但那块「怎么解决」得先真的画出来 —— 停在原地 + 一颗「先进入工作台」才叫不拦路,
7951
+ * 就绪即撤等于把那块提示连同修法一起丢掉,用户进去只会发现「工作台 DIY」整个不在。
7952
+ */
7953
+ function gateActionOf(snapshot) {
7954
+ if (snapshot.phase === "failed") return "hold";
7955
+ if (snapshot.phase !== "ready") return "wait";
7956
+ if ((snapshot.bridgeIssues?.length ?? 0) > 0) return "hold";
7957
+ return snapshot.diyGap === void 0 ? "close" : "hold";
7958
+ }
7920
7959
  function commandRow(t) {
7921
7960
  const row = el("div", BootGate_module_default.commandRow);
7922
7961
  const code = el("code", BootGate_module_default.command);
@@ -7989,13 +8028,32 @@ function createBootGate(ctx, options = {}) {
7989
8028
  const bridgeCommand = commandRow(t);
7990
8029
  const bridgeRetry = el("button", BootGate_module_default.button, t("env.retry"));
7991
8030
  bridgeRetry.setAttribute("type", "button");
8031
+ const bridgeSkip = el("button", BootGate_module_default.button, t("env.bridge.continue"));
8032
+ bridgeSkip.setAttribute("type", "button");
7992
8033
  const bridgeButtons = el("div", BootGate_module_default.buttons);
7993
- bridgeButtons.append(bridgeRetry);
8034
+ bridgeButtons.append(bridgeRetry, bridgeSkip);
7994
8035
  const bridgeRaw = el("details", BootGate_module_default.raw);
7995
8036
  const bridgeRawText = el("pre", BootGate_module_default.error);
7996
8037
  bridgeRaw.append(el("summary", BootGate_module_default.rawSummary, t("env.fix.details")), bridgeRawText);
7997
8038
  bridge.append(bridgeTitle, bridgeWhy, bridgeLines, bridgeCommand.row, bridgeButtons, bridgeRaw);
7998
- card.append(title, hint, list, progressRow, actions, bridge);
8039
+ const diy = el("div", BootGate_module_default.bridge);
8040
+ diy.hidden = true;
8041
+ const diyTitle = el("h2", BootGate_module_default.fixTitle, t("env.fix.title"));
8042
+ const diyWhy = el("p", BootGate_module_default.hint, t("env.diy.why"));
8043
+ const diyLines = el("ul", BootGate_module_default.bridgeLines);
8044
+ const diyStep = el("p", BootGate_module_default.hint, t("env.diy.step"));
8045
+ const diyCommand = commandRow(t);
8046
+ const diyRetry = el("button", BootGate_module_default.button, t("env.retry"));
8047
+ diyRetry.setAttribute("type", "button");
8048
+ const diySkip = el("button", BootGate_module_default.button, t("env.diy.continue"));
8049
+ diySkip.setAttribute("type", "button");
8050
+ const diyButtons = el("div", BootGate_module_default.buttons);
8051
+ diyButtons.append(diyRetry, diySkip);
8052
+ const diyRaw = el("details", BootGate_module_default.raw);
8053
+ const diyRawText = el("pre", BootGate_module_default.error);
8054
+ diyRaw.append(el("summary", BootGate_module_default.rawSummary, t("env.fix.details")), diyRawText);
8055
+ diy.append(diyTitle, diyWhy, diyLines, diyStep, diyCommand.row, diyButtons, diyRaw);
8056
+ card.append(title, hint, list, progressRow, actions, bridge, diy);
7999
8057
  overlay.append(card);
8000
8058
  document.body.append(overlay);
8001
8059
  const previousInert = root?.inert ?? false;
@@ -8053,7 +8111,7 @@ function createBootGate(ctx, options = {}) {
8053
8111
  return row;
8054
8112
  }
8055
8113
  function render(snapshot) {
8056
- hint.textContent = snapshot.phase === "installing" ? t("env.installing") : snapshot.phase === "failed" ? t("env.failed") : t("env.checking");
8114
+ hint.textContent = snapshot.phase === "installing" ? t("env.installing") : snapshot.phase === "failed" ? t("env.failed") : gateActionOf(snapshot) === "hold" ? (snapshot.bridgeIssues?.length ?? 0) > 0 ? t("env.bridge.hold") : t("env.diy.hold") : t("env.checking");
8057
8115
  const items = snapshot.steps.map((step) => {
8058
8116
  const row = ensureRow(step);
8059
8117
  row.item.dataset.status = step.status;
@@ -8111,6 +8169,15 @@ function createBootGate(ctx, options = {}) {
8111
8169
  bridgeRawText.textContent = bridgeStep.detail ?? "";
8112
8170
  bridgeRaw.hidden = (bridgeStep.detail ?? "") === "";
8113
8171
  }
8172
+ const gap = snapshot.diyGap;
8173
+ diy.hidden = gap === void 0;
8174
+ if (gap !== void 0) {
8175
+ const lines = gap.rows.length === 0 ? [t("env.diy.noRow")] : [t("env.diy.rows", { rows: gap.rows.join("、") })];
8176
+ diyLines.replaceChildren(...lines.map((text) => el("li", BootGate_module_default.bridgeLine, text)));
8177
+ diyCommand.set(gap.command);
8178
+ diyRawText.textContent = gap.detail;
8179
+ diyRaw.hidden = gap.detail === "";
8180
+ }
8114
8181
  }
8115
8182
  async function poll() {
8116
8183
  if (cancelled) return;
@@ -8122,19 +8189,22 @@ function createBootGate(ctx, options = {}) {
8122
8189
  return;
8123
8190
  }
8124
8191
  render(response);
8125
- if (response.phase === "ready") {
8192
+ const action = gateActionOf(response);
8193
+ if (action === "close") {
8126
8194
  close();
8127
8195
  return;
8128
8196
  }
8129
- if (response.phase === "failed") return;
8197
+ if (action === "hold") return;
8130
8198
  timer = setTimeout(() => void poll(), POLL_MS);
8131
8199
  }
8132
8200
  function retryNow() {
8133
8201
  forcePlan.again();
8134
8202
  actions.hidden = true;
8135
8203
  bridge.hidden = true;
8204
+ diy.hidden = true;
8136
8205
  retry.setAttribute("disabled", "");
8137
8206
  bridgeRetry.setAttribute("disabled", "");
8207
+ diyRetry.setAttribute("disabled", "");
8138
8208
  hint.textContent = t("env.checking");
8139
8209
  if (pinned !== null) {
8140
8210
  render(pinned);
@@ -8144,6 +8214,9 @@ function createBootGate(ctx, options = {}) {
8144
8214
  }
8145
8215
  retry.addEventListener("click", retryNow);
8146
8216
  bridgeRetry.addEventListener("click", retryNow);
8217
+ bridgeSkip.addEventListener("click", close);
8218
+ diyRetry.addEventListener("click", retryNow);
8219
+ diySkip.addEventListener("click", close);
8147
8220
  const search = typeof window === "undefined" ? "" : window.location?.search ?? "";
8148
8221
  if (devSurface.on(search)) {
8149
8222
  const strip = devSurface.gateStrip({ onPick: (snapshot) => {
@@ -2364,6 +2364,9 @@ function createWorkspaceStore(options) {
2364
2364
  }
2365
2365
  };
2366
2366
  }
2367
+ //#endregion
2368
+ //#region src/shared/api/diy.ts
2369
+ var DIY_PRESET_ID = "workbench-diy";
2367
2370
  var WORKSHOP_RELATIVE = "workbench/diy";
2368
2371
  var WORKSHOP_TITLE = "工作台DIY";
2369
2372
  //#endregion
@@ -2712,8 +2715,23 @@ function readInstalledModule(profileDir, pkg) {
2712
2715
  ...registry === "" ? {} : { registry }
2713
2716
  };
2714
2717
  }
2718
+ var PACKED_ARTIFACT = /\.(?:tgz|tar\.gz|tar)$/i;
2719
+ /**
2720
+ * 这份依赖是不是"从一个**打好的包**装的"(`file:/tmp/dsh-workbench-core-0.3.8.tgz`、
2721
+ * `./x-0.3.8.tgz`)。它跟 `link:` / 目录装的**不是**一回事:那是工作树(改了立刻生效,
2722
+ * 也不可能跟源上比出个新旧),而 tgz 是打包产物 —— 包名与版本就是源上那一套,能比、
2723
+ * 也该让人更新。混在一起会把"从文件装的发布产物"说成"本仓正在开发的那一份",
2724
+ * 顺带把版本比对与更新一起关掉。
2725
+ */
2726
+ function isPackedArtifactSpec(spec) {
2727
+ const trimmed = spec.trim();
2728
+ const pkgPath = trimmed.startsWith("file:") ? trimmed.slice(5) : trimmed;
2729
+ return PACKED_ARTIFACT.test(pkgPath);
2730
+ }
2731
+ /** 这份依赖是不是"本仓那份"(`link:` / 目录 / 相对或绝对路径)—— 只有工作树才算。 */
2715
2732
  function isLocalDependencySpec(spec) {
2716
2733
  const trimmed = spec.trim();
2734
+ if (isPackedArtifactSpec(trimmed)) return false;
2717
2735
  return trimmed.startsWith("link:") || trimmed.startsWith("file:") || trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("/");
2718
2736
  }
2719
2737
  function readProfilePackage(profileDir, pkg) {
@@ -11891,6 +11909,75 @@ function createContextInjector(options) {
11891
11909
  } };
11892
11910
  }
11893
11911
  //#endregion
11912
+ //#region src/shared/api/host/diy-preset-health.ts
11913
+ /**
11914
+ * DIY 模式挂不起来时给用户的那条修法:升级 dsh,再重启一次工作台(启动器那道拒绝用的是同一条)。
11915
+ */
11916
+ var DIY_UPGRADE_COMMAND = "npm i -g @deepseek-ai/dsh@latest";
11917
+ /**
11918
+ * 把 dsh 名册里那句 `broken` 原文拆成一项一个行名。
11919
+ * @param detail - registry 给的原文,一行一项,形如 `tool-schedule (@deepseek-ai/dsh-tool-schedule): never started`。
11920
+ * @returns 每行 `(` 之前那一段;空原文给空数组。
11921
+ */
11922
+ function brokenRowsOf(detail) {
11923
+ const rows = [];
11924
+ for (const line of String(detail ?? "").split("\n")) {
11925
+ const text = line.trim();
11926
+ if (text === "") continue;
11927
+ const at = text.indexOf(" (");
11928
+ rows.push(at < 0 ? text : text.slice(0, at));
11929
+ }
11930
+ return rows;
11931
+ }
11932
+ /**
11933
+ * 从 preset 名册判 DIY 模式挂不挂得起来。
11934
+ * @param presets - `agentPresets.list()` 的回值。
11935
+ * @returns 名册还空着(preset 行还没登记完)给 `undefined` —— 那是"还不知道",不是故障;否则给结论。
11936
+ */
11937
+ function diyPresetVerdictOf(presets) {
11938
+ if (!Array.isArray(presets) || presets.length === 0) return void 0;
11939
+ const mine = presets.find((row) => row?.id === DIY_PRESET_ID);
11940
+ if (mine === void 0) return {
11941
+ ok: false,
11942
+ rows: [],
11943
+ detail: `preset 名单里没有 ${DIY_PRESET_ID}`
11944
+ };
11945
+ const broken = typeof mine.broken === "string" ? mine.broken.trim() : "";
11946
+ if (broken === "") return {
11947
+ ok: true,
11948
+ rows: [],
11949
+ detail: ""
11950
+ };
11951
+ return {
11952
+ ok: false,
11953
+ rows: brokenRowsOf(broken),
11954
+ detail: broken
11955
+ };
11956
+ }
11957
+ /**
11958
+ * 造一个读 preset 名册的探针:名册还没登记完就每 `everyMs` 重读,到 `deadlineMs` 还没读到就放弃。
11959
+ * @param ctx - 宿主上下文(取 `agentPresets` 服务;那个服务不在时给一个恒回 `undefined` 的探针)。
11960
+ * @param options - 两个时间旋钮(测试用):多久放弃、隔多久重读。
11961
+ * @returns 探针:调一次读一轮,`undefined` 表示"这次没读到"。
11962
+ */
11963
+ function createDiyPresetProbe(ctx, options = {}) {
11964
+ const deadlineMs = options.deadlineMs ?? 15e3;
11965
+ const everyMs = options.everyMs ?? 400;
11966
+ return async () => {
11967
+ const registry = ctx.get("agentPresets");
11968
+ if (registry === void 0 || typeof registry.list !== "function") return void 0;
11969
+ const until = Date.now() + deadlineMs;
11970
+ for (;;) {
11971
+ const rows = await registry.list().catch(() => void 0);
11972
+ if (rows === void 0) return void 0;
11973
+ const verdict = diyPresetVerdictOf(rows);
11974
+ if (verdict !== void 0) return verdict;
11975
+ if (Date.now() >= until) return void 0;
11976
+ await new Promise((resolve) => setTimeout(resolve, everyMs));
11977
+ }
11978
+ };
11979
+ }
11980
+ //#endregion
11894
11981
  //#region src/shared/api/host/preflight.ts
11895
11982
  function classifyFailure(text) {
11896
11983
  const lower = text.toLowerCase();
@@ -11953,6 +12040,7 @@ function createPreflight(options) {
11953
12040
  const steps = () => [
11954
12041
  step("opencli"),
11955
12042
  step("bridge"),
12043
+ ...options.probeDiy === void 0 ? [] : [step("diy")],
11956
12044
  ...warmReads().length === 0 ? [] : [step("warm")]
11957
12045
  ];
11958
12046
  let current = {
@@ -12067,6 +12155,31 @@ function createPreflight(options) {
12067
12155
  });
12068
12156
  return again;
12069
12157
  }
12158
+ async function settleDiy(started) {
12159
+ if (started === void 0) return void 0;
12160
+ const verdict = await started.catch(() => void 0);
12161
+ if (verdict === void 0) {
12162
+ set({ steps: current.steps.filter((item) => item.id !== "diy") });
12163
+ return;
12164
+ }
12165
+ if (verdict.ok) {
12166
+ patchStep("diy", {
12167
+ status: "ok",
12168
+ detail: verdict.detail
12169
+ });
12170
+ return;
12171
+ }
12172
+ patchStep("diy", {
12173
+ status: "warn",
12174
+ detail: verdict.detail,
12175
+ command: DIY_UPGRADE_COMMAND
12176
+ });
12177
+ return {
12178
+ rows: verdict.rows,
12179
+ detail: verdict.detail,
12180
+ command: DIY_UPGRADE_COMMAND
12181
+ };
12182
+ }
12070
12183
  async function doRun() {
12071
12184
  set({
12072
12185
  phase: "checking",
@@ -12078,6 +12191,8 @@ function createPreflight(options) {
12078
12191
  status: "running",
12079
12192
  action: `${command} --version`
12080
12193
  });
12194
+ const diyStarted = options.probeDiy?.();
12195
+ if (diyStarted !== void 0) patchStep("diy", { status: "running" });
12081
12196
  let version = await probeVersion();
12082
12197
  if (!version.ok && !version.missing) {
12083
12198
  patchStep("opencli", {
@@ -12149,10 +12264,12 @@ function createPreflight(options) {
12149
12264
  ...bridge.command === void 0 ? {} : { command: bridge.command }
12150
12265
  });
12151
12266
  const settled = await warmSites(bridge);
12267
+ const diyGap = await settleDiy(diyStarted);
12152
12268
  logger?.warn?.(`[dsh-workbench] 环境体检:${current.steps.map((item) => `${item.id}=${item.status}`).join(" ")}`);
12153
12269
  return set({
12154
12270
  phase: "ready",
12155
- ...settled.ok ? {} : { bridgeIssues: settled.issues }
12271
+ ...settled.ok ? {} : { bridgeIssues: settled.issues },
12272
+ ...diyGap === void 0 ? {} : { diyGap }
12156
12273
  });
12157
12274
  }
12158
12275
  function freshReady() {
@@ -12175,6 +12292,24 @@ function createPreflight(options) {
12175
12292
  };
12176
12293
  }
12177
12294
  //#endregion
12295
+ //#region src/shared/api/host/preflight-route.ts
12296
+ /**
12297
+ * 把启动体检接上 `preflight` 那条路由。
12298
+ * @param input - 路由表、宿主上下文、执行缝,以及 opencli 命令 / 装它用的命令(可选)与各套件声明的预热读命令。
12299
+ * @returns 无(路由挂在 `input.routes.preflight` 上)。
12300
+ */
12301
+ function registerPreflight(input) {
12302
+ const preflight = createPreflight({
12303
+ shell: input.shell,
12304
+ command: input.opencliCommand,
12305
+ ...input.installCommand === void 0 ? {} : { installCommand: input.installCommand },
12306
+ logger: input.ctx.logger,
12307
+ warmReads: input.warmReads,
12308
+ probeDiy: createDiyPresetProbe(input.ctx)
12309
+ });
12310
+ input.routes.preflight = preflightHandler(preflight);
12311
+ }
12312
+ //#endregion
12178
12313
  //#region src/shared/api/host/probe-cache.ts
12179
12314
  function createTimedMemo(ttlMs) {
12180
12315
  const entries = /* @__PURE__ */ new Map();
@@ -13980,13 +14115,14 @@ function apply(ctx) {
13980
14115
  }
13981
14116
  if (shell === void 0) ctx.logger?.warn?.("[dsh-workbench] 没有 shell 服务:需要执行缝的套件会降级");
13982
14117
  else {
13983
- routes.preflight = preflightHandler(createPreflight({
14118
+ registerPreflight({
14119
+ routes,
14120
+ ctx,
13984
14121
  shell,
13985
- command: opencli.command,
14122
+ opencliCommand: opencli.command,
13986
14123
  ...installEnv === void 0 ? {} : { installCommand: installEnv },
13987
- logger: ctx.logger,
13988
14124
  warmReads: () => [...mounted.values()].flatMap((item) => item.plan.warmReads())
13989
- }));
14125
+ });
13990
14126
  routes.capability = capabilityHandler(async (unit, force) => {
13991
14127
  if (unit === void 0) return { units: (await probeUnits({
13992
14128
  shell,
@@ -1,4 +1,5 @@
1
1
  import type { Translate } from '@dsh-workbench/shared';
2
+ import type { PreflightSnapshot } from '../../../shared/api/host/preflight';
2
3
  import { apiGet } from '@dsh-workbench/shared';
3
4
  /**
4
5
  * 每一拍轮询带什么参数:平时什么都不带(宿主那边跟着正在跑的那一轮,刚跑出 `ready` 的直接回上一次),
@@ -14,6 +15,13 @@ export declare function createForcePlan(): {
14
15
  export interface BootGate {
15
16
  close(): void;
16
17
  }
18
+ /**
19
+ * 拿到这一拍之后门该怎么办:`close` 撤掉放行、`hold` 停在原地等用户动手、`wait` 接着轮询。
20
+ * 桥接坏了(`bridgeIssues` 非空)与 DIY 模式挂不起来(`diyGap` 非空)都是**不拦路**的,
21
+ * 但那块「怎么解决」得先真的画出来 —— 停在原地 + 一颗「先进入工作台」才叫不拦路,
22
+ * 就绪即撤等于把那块提示连同修法一起丢掉,用户进去只会发现「工作台 DIY」整个不在。
23
+ */
24
+ export declare function gateActionOf(snapshot: PreflightSnapshot): 'close' | 'hold' | 'wait';
17
25
  export declare function createBootGate(ctx: {
18
26
  locale: {
19
27
  bind(namespace: string): Translate;
@@ -0,0 +1,35 @@
1
+ import type { DshAgentPresetRow, DshHostContext } from '@dsh-workbench/shared';
2
+ /**
3
+ * DIY 模式挂不起来时给用户的那条修法:升级 dsh,再重启一次工作台(启动器那道拒绝用的是同一条)。
4
+ */
5
+ export declare const DIY_UPGRADE_COMMAND = "npm i -g @deepseek-ai/dsh@latest";
6
+ /**
7
+ * 一次 DIY 模式体检的结论:`ok` 为真 = 名册里有那一行且挂着;为假时 `rows` 是挂不起来的行名、`detail` 是 dsh 给的原文。
8
+ */
9
+ export interface DiyPresetVerdict {
10
+ readonly ok: boolean;
11
+ readonly rows: readonly string[];
12
+ readonly detail: string;
13
+ }
14
+ /**
15
+ * 把 dsh 名册里那句 `broken` 原文拆成一项一个行名。
16
+ * @param detail - registry 给的原文,一行一项,形如 `tool-schedule (@deepseek-ai/dsh-tool-schedule): never started`。
17
+ * @returns 每行 `(` 之前那一段;空原文给空数组。
18
+ */
19
+ export declare function brokenRowsOf(detail: string): readonly string[];
20
+ /**
21
+ * 从 preset 名册判 DIY 模式挂不挂得起来。
22
+ * @param presets - `agentPresets.list()` 的回值。
23
+ * @returns 名册还空着(preset 行还没登记完)给 `undefined` —— 那是"还不知道",不是故障;否则给结论。
24
+ */
25
+ export declare function diyPresetVerdictOf(presets: readonly DshAgentPresetRow[]): DiyPresetVerdict | undefined;
26
+ /**
27
+ * 造一个读 preset 名册的探针:名册还没登记完就每 `everyMs` 重读,到 `deadlineMs` 还没读到就放弃。
28
+ * @param ctx - 宿主上下文(取 `agentPresets` 服务;那个服务不在时给一个恒回 `undefined` 的探针)。
29
+ * @param options - 两个时间旋钮(测试用):多久放弃、隔多久重读。
30
+ * @returns 探针:调一次读一轮,`undefined` 表示"这次没读到"。
31
+ */
32
+ export declare function createDiyPresetProbe(ctx: DshHostContext, options?: {
33
+ deadlineMs?: number;
34
+ everyMs?: number;
35
+ }): () => Promise<DiyPresetVerdict | undefined>;
@@ -34,6 +34,15 @@ export interface DeclarationGap {
34
34
  export declare function declarationGaps(profileDir: string | null): readonly DeclarationGap[] | undefined;
35
35
  export declare function declaredPluginCommands(profileDir: string | null): ReadonlyMap<string, ReadonlySet<string>>;
36
36
  export declare function readInstalledModule(profileDir: string | null, pkg: string): InstalledModule;
37
+ /**
38
+ * 这份依赖是不是"从一个**打好的包**装的"(`file:/tmp/dsh-workbench-core-0.3.8.tgz`、
39
+ * `./x-0.3.8.tgz`)。它跟 `link:` / 目录装的**不是**一回事:那是工作树(改了立刻生效,
40
+ * 也不可能跟源上比出个新旧),而 tgz 是打包产物 —— 包名与版本就是源上那一套,能比、
41
+ * 也该让人更新。混在一起会把"从文件装的发布产物"说成"本仓正在开发的那一份",
42
+ * 顺带把版本比对与更新一起关掉。
43
+ */
44
+ export declare function isPackedArtifactSpec(spec: string): boolean;
45
+ /** 这份依赖是不是"本仓那份"(`link:` / 目录 / 相对或绝对路径)—— 只有工作树才算。 */
37
46
  export declare function isLocalDependencySpec(spec: string): boolean;
38
47
  export interface ProfilePackage {
39
48
  readonly pkg: string;
@@ -0,0 +1,15 @@
1
+ import type { DshHostContext, DshShell } from '@dsh-workbench/shared';
2
+ import type { RouteTable } from '@dsh-workbench/shared/host';
3
+ /**
4
+ * 把启动体检接上 `preflight` 那条路由。
5
+ * @param input - 路由表、宿主上下文、执行缝,以及 opencli 命令 / 装它用的命令(可选)与各套件声明的预热读命令。
6
+ * @returns 无(路由挂在 `input.routes.preflight` 上)。
7
+ */
8
+ export declare function registerPreflight(input: {
9
+ routes: RouteTable;
10
+ ctx: DshHostContext;
11
+ shell: DshShell;
12
+ opencliCommand: string;
13
+ installCommand?: string | undefined;
14
+ warmReads: () => readonly (readonly string[])[];
15
+ }): void;
@@ -1,5 +1,6 @@
1
1
  import type { DshShell } from '@dsh-workbench/shared';
2
- export type PreflightStepId = 'opencli' | 'bridge' | 'warm';
2
+ import type { DiyPresetVerdict } from './diy-preset-health';
3
+ export type PreflightStepId = 'opencli' | 'bridge' | 'diy' | 'warm';
3
4
  export type PreflightStepStatus = 'pending' | 'running' | 'ok' | 'warn' | 'failed';
4
5
  export interface PreflightStep {
5
6
  readonly id: PreflightStepId;
@@ -17,6 +18,14 @@ export declare function readBridgeReport(stdout: string): {
17
18
  readonly issues: readonly PreflightBridgeIssue[];
18
19
  readonly detail: string;
19
20
  };
21
+ /**
22
+ * DIY 模式挂不起来时随快照送出去的那三件:挂不起来的行、dsh 给的原文、修法(升级 dsh)。
23
+ */
24
+ export interface PreflightDiyGap {
25
+ readonly rows: readonly string[];
26
+ readonly detail: string;
27
+ readonly command: string;
28
+ }
20
29
  export interface PreflightSnapshot {
21
30
  readonly phase: 'idle' | 'checking' | 'installing' | 'ready' | 'failed';
22
31
  readonly steps: readonly PreflightStep[];
@@ -25,6 +34,7 @@ export interface PreflightSnapshot {
25
34
  readonly failure?: PreflightFailure;
26
35
  readonly guidance?: PreflightGuidance;
27
36
  readonly bridgeIssues?: readonly PreflightBridgeIssue[];
37
+ readonly diyGap?: PreflightDiyGap;
28
38
  }
29
39
  /**
30
40
  * 环境体检:`run()` 跑一次并回快照,`snapshot()` 看眼下这一拍。`run` 的取舍是:
@@ -61,4 +71,5 @@ export declare function createPreflight(options: {
61
71
  detail: string;
62
72
  }>) | undefined;
63
73
  warmReads?: (() => readonly (readonly string[])[]) | undefined;
74
+ probeDiy?: (() => Promise<DiyPresetVerdict | undefined>) | undefined;
64
75
  }): Preflight;
package/docs/HANDOFF.md CHANGED
@@ -19,7 +19,7 @@
19
19
 
20
20
  | # | 东西 | 为什么 | 怎么有 |
21
21
  | --- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- |
22
- | 1 | `dsh` | 本体。本仓库在 `0.1.7-rc.2` 上验过 | `npm i -g @deepseek-ai/dsh`(公网 npm 上有) |
22
+ | 1 | `dsh` | 本体。本仓库在 `0.2.1-rc.2` 上验过,**低于它的会被拒绝** —— 见第八节 | `npm i -g @deepseek-ai/dsh@latest`(公网 npm 上有) |
23
23
  | 2 | `node` + **`pnpm`** | dsh 的 `plugin` 子命令(以及 web 的 Plugins 页)都是**转发给 pnpm** 的;没有 pnpm 会直接报 `dsh: pnpm was not found` | 各自装 |
24
24
  | 3 | 跑过一次 `dsh web` | 走 A 需要它给出那个 profile;走 B 时 `setup` 拿 `~/.dsh/profiles/web` 当模板。没有它会**明确报错**,不会静默建出半个 profile | `dsh web` |
25
25
  | 4 | 一个能解析这个包的源 | 包在**公网 npm** 上,默认源即可。只有将来改发内网源时,才要 `npm config set @<scope>:registry …` + `npm login`(带 scope 的包只去 `.npmrc` 里给这个 scope 指的源找) | `npm view @dsh-workbench/core version` 出号即可 |
@@ -239,9 +239,26 @@ manifest;彩排结束、以及「零上传」的失败,都会自动回滚。
239
239
 
240
240
  ## 八、版本与兼容(实话)
241
241
 
242
- `package.json` 的 `peerDependencies` 里**没有** `@deepseek-ai/dsh`,这是有意的:
243
- dsh 现在发的是预发布(npm 的 `next` 频道;本仓**验过的**是 `0.2.1-rc.2`,2026-10-18),
244
- 声明范围会让版本不一致的人**装不上**;不声明则是"谁都能装,坏了当场知道"。
242
+ 外壳在 `package.json` 里声明了 **`engines.dsh: ">=0.2.1-rc.2"`**(本仓验过的那一代,2026-10-18)。
243
+ 这条声明是后补的,来历见
244
+ [`.scratch/dsh-version-floor/spec.md`](../../../.scratch/dsh-version-floor/spec.md):真实用户装了
245
+ `dsh 0.2.0-rc.2`(当时 npm `latest` 还是它)+ 工作台,工作台照常起得来,**只有 DIY 模式整个不见、
246
+ 一句错都不报** —— `preset/diy.patch.yml` 是从 dsh 0.2.1-rc.2 那份 `standard.patch.yml` 派生的,
247
+ 里面一行 `tool-schedule` 在 0.2.0 上根本不存在,preset 挂不起来;而 dsh 的模式选择器**只列挂得起来的**
248
+ (healthy presets only),于是"工作台 DIY"从用户面前消失。同一代的 `uiWorkspace.startSession()`
249
+ 也**不收** `agentPreset`(0.2.1-alpha.3 才收),所以这不是"少一把工具"的降级档,是 DIY 模式整块不可用。
250
+
251
+ 因此**声明一个下限**(而不是"谁都能装"):dsh 自 `0.2.1-alpha.4` 起预检**先读 `engines.dsh`**
252
+ (声明了它,peer 就被整体忽略;写 `workspace:` 一律判不兼容;`engines` 不是对象或 `engines.dsh`
253
+ 不是字符串会让清单不可读并**直接拒绝**),不满足就拒绝安装,
254
+ 可用 `dsh plugin --profile workbench allow-version <包@版本> --dsh-version <版本> --accept-risk` 豁免。
255
+ 旧到不认识 `engines.dsh` 的 dsh(0.2.0 及更早只读 `peerDependencies`)由**启动器那道硬门禁**兜
256
+ (`bin/dsh-workbench`,与"bundle 没被选中"同一档:拒绝启动 + 打印升级命令)。
257
+
258
+ ⚠️ **别把下限改写成 `peerDependencies['@deepseek-ai/dsh']`**:pnpm 的 `auto-install-peers`
259
+ (v11 实测:连 `peerDependenciesMeta.optional` 的 peer 也照装)会把**整套 dsh**拖进 profile。
260
+ 下限只有一处真相(`package.json` 的 `engines.dsh`),启动器读它、不另写版本号 —— selfcheck 有一条判据
261
+ 钉着"下限 = `@deepseek-ai/dsh-client-ui-primitives` 那个 pin"。
245
262
 
246
263
  代价要清楚:工作台依赖 dsh 的注入面(`@deepseek-ai/dsh-client-ui-slots`、
247
264
  `@deepseek-ai/dsh-client-locale`,见 `dsh.client.inject`)与一批**按名字取的服务**
@@ -253,20 +270,8 @@ dsh 现在发的是预发布(npm 的 `next` 频道;本仓**验过的**是 `0
253
270
  **devDependency 里的 `@deepseek-ai/dsh-client-ui-primitives` 跟着"验过的 dsh 版本"走**
254
271
  ([ADR-0028 §4.1](../../../docs/adr/0028-workbench-ui-uses-dsh-primitives.md)):它只出类型,
255
272
  但类型比运行时旧一代,编译期就替不了你把关。当前两份都钉 `0.2.1-rc.2`
256
- (`packages/workbench` / `packages/shared`;改完要跑一次 `pnpm install` 更新 `pnpm-lock.yaml`)。
257
-
258
- 想改成"装的时候就拦住",在 `package.json` 里声明:
259
-
260
- ```json
261
- "peerDependencies": { "@deepseek-ai/dsh": "^0.2.1-rc.2" }
262
- ```
263
-
264
- dsh 会用这条做**安装预检**,不满足就拒绝安装,
265
- 可用 `dsh plugin --profile workbench allow-version <包@版本> --dsh-version <版本> --accept-risk` 豁免。
266
- **自 0.2.1-alpha.4 起预检先读包自己的 `engines.dsh`**:声明了它,上面这条 peer 就被整体忽略
267
- (只有没有 `engines.dsh` 时才看 peer;`engines.dsh` 里写 `workspace:` 一律判不兼容,
268
- `engines` 不是对象或 `engines.dsh` 不是字符串会让清单不可读并**直接拒绝**)。要支持一段 dsh 版本,
269
- 写 `engines.dsh` 比写 peer 更直接。
273
+ (`packages/workbench` / `packages/shared`;改完要跑一次 `pnpm install` 更新 `pnpm-lock.yaml`),
274
+ **升这一份时要连 `engines.dsh` 一起升**(selfcheck 那条判据把两者钉在一起)。
270
275
 
271
276
  > `NPM_REGISTRY` 是**你们自己的**内网 npm 源地址(形如 `https://<源主机>/<路径>/`)。
272
277
  > 本仓不写它:开源包里不该出现任何内网地址。
@@ -26,6 +26,9 @@
26
26
  `OPENCLI_PIN` 锁),但它连的 **daemon 是整机一个** —— 见下面「隔离边界」。
27
27
 
28
28
  前提:`dsh` 和 `pnpm` 在 PATH 上(`dsh plugin` 是转发给 pnpm 的),
29
+ **`dsh` 不低于验过的那一代**(`engines.dsh`;旧到不认识它的 dsh 由启动器当场拒绝并给
30
+ `npm i -g @deepseek-ai/dsh@latest` —— 低于它的 dsh 上 DIY 模式整个不出现且不报错,见
31
+ [`.scratch/dsh-version-floor/spec.md`](../../../.scratch/dsh-version-floor/spec.md)),
29
32
  且 `~/.dsh/profiles/web` 已存在(跑过一次 `dsh web` 即可)—— seed 拿它当**模板**
30
33
  (bundle 清单),之后也是「配置同步」的默认来源。
31
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/core",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "个人工作台:Agent 工作台外壳(左栏 + 常驻对话 + 自研右侧数据面板)。",
5
5
  "type": "module",
6
6
  "icon": "./icon.svg",
@@ -62,6 +62,10 @@
62
62
  ]
63
63
  }
64
64
  },
65
+ "//engines": "DIY 模式(preset/diy.patch.yml)是从 **dsh 出厂那份 standard.patch.yml** 派生的,所以这份外壳只跟着它派生的那一代 dsh 走:dsh 太旧时那个 preset 挂不起来,而 dsh 的模式选择器**只列挂得起来的** ⇒ 用户面前「工作台 DIY」整个不见,一句错都不报(实测 0.2.0-rc.2:缺 dsh-tool-schedule)。这里声明下限,让 dsh 自己的兼容性预检在**装的时候**拒绝(自 0.2.1-alpha.4 起 engines.dsh 优先于 peer)。⚠️ **不写 peerDependencies['@deepseek-ai/dsh']**:pnpm 的 auto-install-peers(v11 连 optional 也装)会把整套 dsh 拖进 profile。旧到不认识 engines.dsh 的 dsh(0.2.0 及更早)由启动器那道硬门禁兜 —— 见 bin/dsh-workbench。判据:selfcheck 的「声明下限 = 验过的 dsh 版本」那条。",
66
+ "engines": {
67
+ "dsh": ">=0.2.1-rc.2"
68
+ },
65
69
  "peerDependencies": {
66
70
  "react": "*"
67
71
  },
@@ -87,7 +91,7 @@
87
91
  "access": "public"
88
92
  },
89
93
  "dependencies": {
90
- "@dsh-workbench/shared": "0.3.8"
94
+ "@dsh-workbench/shared": "0.3.9"
91
95
  },
92
96
  "scripts": {
93
97
  "build": "node scripts/build.mjs && node scripts/build-types.mjs",
@@ -28,6 +28,12 @@ DST_PROFILE="$DSH_HOME/profiles/$PROFILE"
28
28
  PKG='@dsh-workbench/core'
29
29
  REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
30
30
  GIT_TOP="$(git -C "$REPO_DIR" rev-parse --show-toplevel 2>/dev/null || true)"
31
+ # 同 bin/dsh-workbench 那一处(那边写清了为什么):`--show-toplevel` 只说明**某个祖先**是仓库,
32
+ # 装出来的那份可能刚好落在某个仓库里(`$HOME` 是 dotfiles 仓库时最常见)—— 还要证明这份
33
+ # package.json 本身被那个仓库跟踪,才算是仓库副本。
34
+ if [ -n "$GIT_TOP" ] && ! git -C "$REPO_DIR" ls-files --error-unmatch package.json >/dev/null 2>&1; then
35
+ GIT_TOP=""
36
+ fi
31
37
  ISOLATION="$REPO_DIR/profile/isolation.patch.yml"
32
38
 
33
39
  # `verify.sh` 留在**仓库根**(它证明的是"dsh web 未被动过 / 组合树差异 / 数据分家",
@@ -134,6 +140,34 @@ fs.writeFileSync(process.env.WB_MANIFEST, JSON.stringify(wb, null, 2) + "\n");
134
140
  console.log("→ 已继承 web 的 bundle 列表: " + merged.join(", "));
135
141
  '
136
142
 
143
+ # ── 第 2.5 步:修掉旧版启动器留下的"装出来的那份被当成仓库副本" ──────────
144
+ #
145
+ # 旧判据只问 `git rev-parse --show-toplevel`("**某个祖先**是不是仓库"),于是包目录落在任何
146
+ # 仓库里的机器(最常见:`$HOME` 是 dotfiles 仓库,而 npm/nvm 的前缀就在 `$HOME` 下)在 setup
147
+ # 时会把**全局安装目录**以 `link:` 装进 profile —— 市场里工作台那一行因此说「这份是本仓正在
148
+ # 开发的那一份,不从源上取。」(不比对源、不提示更新),而 `npm i -g …@latest` 还会悄悄换掉
149
+ # profile 跑的东西。判据窄到只认那一个坏形状:**我自己是装出来的那份**(GIT_TOP 为空)
150
+ # **且 profile 里那条 `link:` 指着的正是我自己这份目录** —— 指别处的 link:(用户自己链的)不动。
151
+ if [ -z "$GIT_TOP" ] && [ -f "$DST_PROFILE/package.json" ]; then
152
+ WB_MANIFEST="$DST_PROFILE/package.json" WB_PKG="$PKG" WB_SELF="$REPO_DIR" \
153
+ WB_TARGET="$PKG@^$(node -p "require('$REPO_DIR/package.json').version" 2>/dev/null || echo 0)" \
154
+ node -e '
155
+ const fs = require("node:fs");
156
+ const path = require("node:path");
157
+ const file = process.env.WB_MANIFEST;
158
+ const manifest = JSON.parse(fs.readFileSync(file, "utf8"));
159
+ const deps = manifest.dependencies ?? {};
160
+ const spec = String(deps[process.env.WB_PKG] ?? "");
161
+ if (!spec.startsWith("link:")) process.exit(0);
162
+ const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
163
+ const target = path.resolve(path.dirname(file), spec.slice("link:".length));
164
+ if (real(target) !== real(process.env.WB_SELF)) process.exit(0);
165
+ manifest.dependencies = Object.assign({}, deps, { [process.env.WB_PKG]: process.env.WB_TARGET });
166
+ fs.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n");
167
+ console.log("→ profile 里那条 link: 指的是我这份装出来的副本(旧启动器判错的产物)—— 已改回 " + process.env.WB_TARGET);
168
+ '
169
+ fi
170
+
137
171
  # ── 第 3 步:追加会话 / 存储 / 附件隔离层 ───────────────────────────────
138
172
  if grep -q 'id: session-persistence-jsonl' "$DST_PATCH"; then
139
173
  echo "→ 隔离层已存在,跳过"