@dsh-workbench/core 0.3.7 → 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
@@ -559,6 +559,16 @@ window.__ModuleLoader__.load({
559
559
  return "idle";
560
560
  }
561
561
  /**
562
+ * 这一趟还在不在跑(`running` 与 `needs-login` 都算)。
563
+ * 卡片要判"这一行要不要摆停止钮""这套环境里有没有活着的跑"时用这个 —— **不是** `longTaskDot`:
564
+ * 那一件是状态点的**长相**(四档),归 `LongTaskRow` 自己用,不该被调用点拿去当判据。
565
+ * @param status 长任务的五种态。
566
+ * @returns 在跑 `true`;成功 / 失败 / 用户按停都是 `false`。
567
+ */
568
+ function longTaskIsLive(status) {
569
+ return status === "running" || status === "needs-login";
570
+ }
571
+ /**
562
572
  * 算停止钮现在在哪一档。
563
573
  * @param state armed = 已经按过第一下、在等第二下;busy = 停止请求已经在飞。
564
574
  * @returns busy 优先(在飞时按不动),其次 armed,否则 idle。
@@ -2771,7 +2781,8 @@ window.__ModuleLoader__.load({
2771
2781
  "env.stepFailed.shell": "执行通道不可用",
2772
2782
  "env.bridge.ok": "已连接",
2773
2783
  "env.bridge.short": "没连上",
2774
- "env.bridge.why": "面板的数据要走你浏览器里的登录态:这个通道没连上,模块就取不到数据(不拦路,但先修好它)。",
2784
+ "env.bridge.why": "面板的数据要走你浏览器里的登录态:这个通道没连上,模块就取不到数据(不拦路:修好它,或者先进入工作台)。",
2785
+ "env.bridge.hold": "浏览器通道还没连上 —— 按下面修好后点「重试」,也可以先进工作台。",
2775
2786
  "env.bridge.daemon": "后台服务没起来:正常它会自己起,先在终端跑下面这条命令。",
2776
2787
  "env.bridge.extension": "浏览器扩展没连上:在 Chrome 应用商店装一次就好(装完回来点「重试」)。已经装过的话,先跑下面这条命令。",
2777
2788
  "env.bridge.extensionLink": "去 Chrome 应用商店装扩展",
@@ -2789,11 +2800,21 @@ window.__ModuleLoader__.load({
2789
2800
  "env.fix.network": "网络不通:检查网络或代理设置后重试。",
2790
2801
  "env.fix.details": "详细输出(排障用)",
2791
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": "先进入工作台",
2792
2812
  "env.copyCommand": "复制命令",
2793
2813
  "env.copied": "已复制",
2794
2814
  "env.step.opencli": "检测工作台的 opencli",
2795
2815
  "env.step.codingPlugin": "检测 coding 插件",
2796
2816
  "env.step.bridge": "检测浏览器通道",
2817
+ "env.step.diy": "检查 DIY 模式",
2797
2818
  "env.step.warm": "预热站点登录态",
2798
2819
  "env.warm.done": "已预热 {n} 个站点",
2799
2820
  "env.warm.short": "这次没热上 —— 不影响用,面板第一次读取会自己补",
@@ -3215,7 +3236,8 @@ window.__ModuleLoader__.load({
3215
3236
  "env.stepFailed.shell": "no shell channel",
3216
3237
  "env.bridge.ok": "Connected",
3217
3238
  "env.bridge.short": "Not connected",
3218
- "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.",
3219
3241
  "env.bridge.daemon": "The background service is not running. It usually starts by itself — run the command below in your terminal first.",
3220
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.",
3221
3243
  "env.bridge.extensionLink": "Get the extension in the Chrome Web Store",
@@ -3233,11 +3255,21 @@ window.__ModuleLoader__.load({
3233
3255
  "env.fix.network": "Network unreachable: check your network or proxy, then retry.",
3234
3256
  "env.fix.details": "Details (for troubleshooting)",
3235
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",
3236
3267
  "env.copyCommand": "Copy command",
3237
3268
  "env.copied": "Copied",
3238
3269
  "env.step.opencli": "Checking the workbench opencli",
3239
3270
  "env.step.codingPlugin": "Checking the coding plugin",
3240
3271
  "env.step.bridge": "Checking the browser bridge",
3272
+ "env.step.diy": "Checking DIY mode",
3241
3273
  "env.step.warm": "Warming up site sessions",
3242
3274
  "env.warm.done": "{n} site(s) warmed up",
3243
3275
  "env.warm.short": "Not warmed this time — harmless; the panel heals on first read",
@@ -7789,6 +7821,10 @@ window.__ModuleLoader__.load({
7789
7821
  if (step.status === "ok") return t("env.bridge.ok");
7790
7822
  if (step.status === "warn") return t("env.bridge.short");
7791
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
+ }
7792
7828
  if (step.id === "warm") {
7793
7829
  if (step.status === "ok") return t("env.warm.done", { n: step.detail ?? "0" });
7794
7830
  if (step.status === "warn") return t("env.warm.short");
@@ -7884,6 +7920,7 @@ window.__ModuleLoader__.load({
7884
7920
  function stepLabel$1(id, t) {
7885
7921
  if (id === "opencli") return t("env.step.opencli");
7886
7922
  if (id === "warm") return t("env.step.warm");
7923
+ if (id === "diy") return t("env.step.diy");
7887
7924
  return t("env.step.bridge");
7888
7925
  }
7889
7926
  function stepMark(step) {
@@ -7911,6 +7948,18 @@ window.__ModuleLoader__.load({
7911
7948
  }
7912
7949
  };
7913
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
+ }
7914
7963
  function commandRow(t) {
7915
7964
  const row = el("div", BootGate_module_default.commandRow);
7916
7965
  const code = el("code", BootGate_module_default.command);
@@ -7983,13 +8032,32 @@ window.__ModuleLoader__.load({
7983
8032
  const bridgeCommand = commandRow(t);
7984
8033
  const bridgeRetry = el("button", BootGate_module_default.button, t("env.retry"));
7985
8034
  bridgeRetry.setAttribute("type", "button");
8035
+ const bridgeSkip = el("button", BootGate_module_default.button, t("env.bridge.continue"));
8036
+ bridgeSkip.setAttribute("type", "button");
7986
8037
  const bridgeButtons = el("div", BootGate_module_default.buttons);
7987
- bridgeButtons.append(bridgeRetry);
8038
+ bridgeButtons.append(bridgeRetry, bridgeSkip);
7988
8039
  const bridgeRaw = el("details", BootGate_module_default.raw);
7989
8040
  const bridgeRawText = el("pre", BootGate_module_default.error);
7990
8041
  bridgeRaw.append(el("summary", BootGate_module_default.rawSummary, t("env.fix.details")), bridgeRawText);
7991
8042
  bridge.append(bridgeTitle, bridgeWhy, bridgeLines, bridgeCommand.row, bridgeButtons, bridgeRaw);
7992
- 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);
7993
8061
  overlay.append(card);
7994
8062
  document.body.append(overlay);
7995
8063
  const previousInert = root?.inert ?? false;
@@ -8047,7 +8115,7 @@ window.__ModuleLoader__.load({
8047
8115
  return row;
8048
8116
  }
8049
8117
  function render(snapshot) {
8050
- 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");
8051
8119
  const items = snapshot.steps.map((step) => {
8052
8120
  const row = ensureRow(step);
8053
8121
  row.item.dataset.status = step.status;
@@ -8105,6 +8173,15 @@ window.__ModuleLoader__.load({
8105
8173
  bridgeRawText.textContent = bridgeStep.detail ?? "";
8106
8174
  bridgeRaw.hidden = (bridgeStep.detail ?? "") === "";
8107
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
+ }
8108
8185
  }
8109
8186
  async function poll() {
8110
8187
  if (cancelled) return;
@@ -8116,19 +8193,22 @@ window.__ModuleLoader__.load({
8116
8193
  return;
8117
8194
  }
8118
8195
  render(response);
8119
- if (response.phase === "ready") {
8196
+ const action = gateActionOf(response);
8197
+ if (action === "close") {
8120
8198
  close();
8121
8199
  return;
8122
8200
  }
8123
- if (response.phase === "failed") return;
8201
+ if (action === "hold") return;
8124
8202
  timer = setTimeout(() => void poll(), POLL_MS);
8125
8203
  }
8126
8204
  function retryNow() {
8127
8205
  forcePlan.again();
8128
8206
  actions.hidden = true;
8129
8207
  bridge.hidden = true;
8208
+ diy.hidden = true;
8130
8209
  retry.setAttribute("disabled", "");
8131
8210
  bridgeRetry.setAttribute("disabled", "");
8211
+ diyRetry.setAttribute("disabled", "");
8132
8212
  hint.textContent = t("env.checking");
8133
8213
  if (pinned !== null) {
8134
8214
  render(pinned);
@@ -8138,6 +8218,9 @@ window.__ModuleLoader__.load({
8138
8218
  }
8139
8219
  retry.addEventListener("click", retryNow);
8140
8220
  bridgeRetry.addEventListener("click", retryNow);
8221
+ bridgeSkip.addEventListener("click", close);
8222
+ diyRetry.addEventListener("click", retryNow);
8223
+ diySkip.addEventListener("click", close);
8141
8224
  const search = typeof window === "undefined" ? "" : window.location?.search ?? "";
8142
8225
  if (devSurface.on(search)) {
8143
8226
  const strip = devSurface.gateStrip({ onPick: (snapshot) => {
@@ -10837,17 +10920,19 @@ window.__ModuleLoader__.load({
10837
10920
  setStopping((current) => current === id ? null : current);
10838
10921
  }
10839
10922
  }, [send]);
10840
- const every = longTaskPollMs(runs.some((run) => longTaskDot(run.status) === "ongoing"), options.pollMs);
10923
+ const every = longTaskPollMs(runs.some((run) => longTaskIsLive(run.status)), options.pollMs);
10841
10924
  useEffect(() => {
10842
10925
  if (!enabled) return void 0;
10843
10926
  const timer = setInterval(() => setTick((count) => count + 1), every);
10844
10927
  return () => clearInterval(timer);
10845
10928
  }, [enabled, every]);
10929
+ const refresh = useCallback(() => setTick((count) => count + 1), []);
10846
10930
  return {
10847
10931
  runs,
10848
10932
  stop,
10849
10933
  stopping,
10850
- stopFailed: write.failed
10934
+ stopFailed: write.failed,
10935
+ refresh
10851
10936
  };
10852
10937
  }
10853
10938
  //#endregion
@@ -11103,6 +11188,7 @@ window.__ModuleLoader__.load({
11103
11188
  ...context_chips_exports
11104
11189
  },
11105
11190
  lib: {
11191
+ longTaskIsLive,
11106
11192
  MIN_BUSY_MS: 500,
11107
11193
  blockerOfCapability,
11108
11194
  blockerOfError,
@@ -552,6 +552,16 @@ function longTaskDot(status) {
552
552
  return "idle";
553
553
  }
554
554
  /**
555
+ * 这一趟还在不在跑(`running` 与 `needs-login` 都算)。
556
+ * 卡片要判"这一行要不要摆停止钮""这套环境里有没有活着的跑"时用这个 —— **不是** `longTaskDot`:
557
+ * 那一件是状态点的**长相**(四档),归 `LongTaskRow` 自己用,不该被调用点拿去当判据。
558
+ * @param status 长任务的五种态。
559
+ * @returns 在跑 `true`;成功 / 失败 / 用户按停都是 `false`。
560
+ */
561
+ function longTaskIsLive(status) {
562
+ return status === "running" || status === "needs-login";
563
+ }
564
+ /**
555
565
  * 算停止钮现在在哪一档。
556
566
  * @param state armed = 已经按过第一下、在等第二下;busy = 停止请求已经在飞。
557
567
  * @returns busy 优先(在飞时按不动),其次 armed,否则 idle。
@@ -2764,7 +2774,8 @@ var dictionaries = {
2764
2774
  "env.stepFailed.shell": "执行通道不可用",
2765
2775
  "env.bridge.ok": "已连接",
2766
2776
  "env.bridge.short": "没连上",
2767
- "env.bridge.why": "面板的数据要走你浏览器里的登录态:这个通道没连上,模块就取不到数据(不拦路,但先修好它)。",
2777
+ "env.bridge.why": "面板的数据要走你浏览器里的登录态:这个通道没连上,模块就取不到数据(不拦路:修好它,或者先进入工作台)。",
2778
+ "env.bridge.hold": "浏览器通道还没连上 —— 按下面修好后点「重试」,也可以先进工作台。",
2768
2779
  "env.bridge.daemon": "后台服务没起来:正常它会自己起,先在终端跑下面这条命令。",
2769
2780
  "env.bridge.extension": "浏览器扩展没连上:在 Chrome 应用商店装一次就好(装完回来点「重试」)。已经装过的话,先跑下面这条命令。",
2770
2781
  "env.bridge.extensionLink": "去 Chrome 应用商店装扩展",
@@ -2782,11 +2793,21 @@ var dictionaries = {
2782
2793
  "env.fix.network": "网络不通:检查网络或代理设置后重试。",
2783
2794
  "env.fix.details": "详细输出(排障用)",
2784
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": "先进入工作台",
2785
2805
  "env.copyCommand": "复制命令",
2786
2806
  "env.copied": "已复制",
2787
2807
  "env.step.opencli": "检测工作台的 opencli",
2788
2808
  "env.step.codingPlugin": "检测 coding 插件",
2789
2809
  "env.step.bridge": "检测浏览器通道",
2810
+ "env.step.diy": "检查 DIY 模式",
2790
2811
  "env.step.warm": "预热站点登录态",
2791
2812
  "env.warm.done": "已预热 {n} 个站点",
2792
2813
  "env.warm.short": "这次没热上 —— 不影响用,面板第一次读取会自己补",
@@ -3208,7 +3229,8 @@ var dictionaries = {
3208
3229
  "env.stepFailed.shell": "no shell channel",
3209
3230
  "env.bridge.ok": "Connected",
3210
3231
  "env.bridge.short": "Not connected",
3211
- "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.",
3212
3234
  "env.bridge.daemon": "The background service is not running. It usually starts by itself — run the command below in your terminal first.",
3213
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.",
3214
3236
  "env.bridge.extensionLink": "Get the extension in the Chrome Web Store",
@@ -3226,11 +3248,21 @@ var dictionaries = {
3226
3248
  "env.fix.network": "Network unreachable: check your network or proxy, then retry.",
3227
3249
  "env.fix.details": "Details (for troubleshooting)",
3228
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",
3229
3260
  "env.copyCommand": "Copy command",
3230
3261
  "env.copied": "Copied",
3231
3262
  "env.step.opencli": "Checking the workbench opencli",
3232
3263
  "env.step.codingPlugin": "Checking the coding plugin",
3233
3264
  "env.step.bridge": "Checking the browser bridge",
3265
+ "env.step.diy": "Checking DIY mode",
3234
3266
  "env.step.warm": "Warming up site sessions",
3235
3267
  "env.warm.done": "{n} site(s) warmed up",
3236
3268
  "env.warm.short": "Not warmed this time — harmless; the panel heals on first read",
@@ -7782,6 +7814,10 @@ function stepDetailText(step, snapshot, t) {
7782
7814
  if (step.status === "ok") return t("env.bridge.ok");
7783
7815
  if (step.status === "warn") return t("env.bridge.short");
7784
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
+ }
7785
7821
  if (step.id === "warm") {
7786
7822
  if (step.status === "ok") return t("env.warm.done", { n: step.detail ?? "0" });
7787
7823
  if (step.status === "warn") return t("env.warm.short");
@@ -7877,6 +7913,7 @@ var GIVE_UP_MS = 12e4;
7877
7913
  function stepLabel$1(id, t) {
7878
7914
  if (id === "opencli") return t("env.step.opencli");
7879
7915
  if (id === "warm") return t("env.step.warm");
7916
+ if (id === "diy") return t("env.step.diy");
7880
7917
  return t("env.step.bridge");
7881
7918
  }
7882
7919
  function stepMark(step) {
@@ -7904,6 +7941,18 @@ function createForcePlan() {
7904
7941
  }
7905
7942
  };
7906
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
+ }
7907
7956
  function commandRow(t) {
7908
7957
  const row = el("div", BootGate_module_default.commandRow);
7909
7958
  const code = el("code", BootGate_module_default.command);
@@ -7976,13 +8025,32 @@ function createBootGate(ctx, options = {}) {
7976
8025
  const bridgeCommand = commandRow(t);
7977
8026
  const bridgeRetry = el("button", BootGate_module_default.button, t("env.retry"));
7978
8027
  bridgeRetry.setAttribute("type", "button");
8028
+ const bridgeSkip = el("button", BootGate_module_default.button, t("env.bridge.continue"));
8029
+ bridgeSkip.setAttribute("type", "button");
7979
8030
  const bridgeButtons = el("div", BootGate_module_default.buttons);
7980
- bridgeButtons.append(bridgeRetry);
8031
+ bridgeButtons.append(bridgeRetry, bridgeSkip);
7981
8032
  const bridgeRaw = el("details", BootGate_module_default.raw);
7982
8033
  const bridgeRawText = el("pre", BootGate_module_default.error);
7983
8034
  bridgeRaw.append(el("summary", BootGate_module_default.rawSummary, t("env.fix.details")), bridgeRawText);
7984
8035
  bridge.append(bridgeTitle, bridgeWhy, bridgeLines, bridgeCommand.row, bridgeButtons, bridgeRaw);
7985
- 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);
7986
8054
  overlay.append(card);
7987
8055
  document.body.append(overlay);
7988
8056
  const previousInert = root?.inert ?? false;
@@ -8040,7 +8108,7 @@ function createBootGate(ctx, options = {}) {
8040
8108
  return row;
8041
8109
  }
8042
8110
  function render(snapshot) {
8043
- 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");
8044
8112
  const items = snapshot.steps.map((step) => {
8045
8113
  const row = ensureRow(step);
8046
8114
  row.item.dataset.status = step.status;
@@ -8098,6 +8166,15 @@ function createBootGate(ctx, options = {}) {
8098
8166
  bridgeRawText.textContent = bridgeStep.detail ?? "";
8099
8167
  bridgeRaw.hidden = (bridgeStep.detail ?? "") === "";
8100
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
+ }
8101
8178
  }
8102
8179
  async function poll() {
8103
8180
  if (cancelled) return;
@@ -8109,19 +8186,22 @@ function createBootGate(ctx, options = {}) {
8109
8186
  return;
8110
8187
  }
8111
8188
  render(response);
8112
- if (response.phase === "ready") {
8189
+ const action = gateActionOf(response);
8190
+ if (action === "close") {
8113
8191
  close();
8114
8192
  return;
8115
8193
  }
8116
- if (response.phase === "failed") return;
8194
+ if (action === "hold") return;
8117
8195
  timer = setTimeout(() => void poll(), POLL_MS);
8118
8196
  }
8119
8197
  function retryNow() {
8120
8198
  forcePlan.again();
8121
8199
  actions.hidden = true;
8122
8200
  bridge.hidden = true;
8201
+ diy.hidden = true;
8123
8202
  retry.setAttribute("disabled", "");
8124
8203
  bridgeRetry.setAttribute("disabled", "");
8204
+ diyRetry.setAttribute("disabled", "");
8125
8205
  hint.textContent = t("env.checking");
8126
8206
  if (pinned !== null) {
8127
8207
  render(pinned);
@@ -8131,6 +8211,9 @@ function createBootGate(ctx, options = {}) {
8131
8211
  }
8132
8212
  retry.addEventListener("click", retryNow);
8133
8213
  bridgeRetry.addEventListener("click", retryNow);
8214
+ bridgeSkip.addEventListener("click", close);
8215
+ diyRetry.addEventListener("click", retryNow);
8216
+ diySkip.addEventListener("click", close);
8134
8217
  const search = typeof window === "undefined" ? "" : window.location?.search ?? "";
8135
8218
  if (devSurface.on(search)) {
8136
8219
  const strip = devSurface.gateStrip({ onPick: (snapshot) => {
@@ -10830,17 +10913,19 @@ function useLongTask(options) {
10830
10913
  setStopping((current) => current === id ? null : current);
10831
10914
  }
10832
10915
  }, [send]);
10833
- const every = longTaskPollMs(runs.some((run) => longTaskDot(run.status) === "ongoing"), options.pollMs);
10916
+ const every = longTaskPollMs(runs.some((run) => longTaskIsLive(run.status)), options.pollMs);
10834
10917
  useEffect(() => {
10835
10918
  if (!enabled) return void 0;
10836
10919
  const timer = setInterval(() => setTick((count) => count + 1), every);
10837
10920
  return () => clearInterval(timer);
10838
10921
  }, [enabled, every]);
10922
+ const refresh = useCallback(() => setTick((count) => count + 1), []);
10839
10923
  return {
10840
10924
  runs,
10841
10925
  stop,
10842
10926
  stopping,
10843
- stopFailed: write.failed
10927
+ stopFailed: write.failed,
10928
+ refresh
10844
10929
  };
10845
10930
  }
10846
10931
  //#endregion
@@ -11096,6 +11181,7 @@ var kit = {
11096
11181
  ...context_chips_exports
11097
11182
  },
11098
11183
  lib: {
11184
+ longTaskIsLive,
11099
11185
  MIN_BUSY_MS: 500,
11100
11186
  blockerOfCapability,
11101
11187
  blockerOfError,