@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.
@@ -555,6 +555,16 @@ function longTaskDot(status) {
555
555
  return "idle";
556
556
  }
557
557
  /**
558
+ * 这一趟还在不在跑(`running` 与 `needs-login` 都算)。
559
+ * 卡片要判"这一行要不要摆停止钮""这套环境里有没有活着的跑"时用这个 —— **不是** `longTaskDot`:
560
+ * 那一件是状态点的**长相**(四档),归 `LongTaskRow` 自己用,不该被调用点拿去当判据。
561
+ * @param status 长任务的五种态。
562
+ * @returns 在跑 `true`;成功 / 失败 / 用户按停都是 `false`。
563
+ */
564
+ function longTaskIsLive(status) {
565
+ return status === "running" || status === "needs-login";
566
+ }
567
+ /**
558
568
  * 算停止钮现在在哪一档。
559
569
  * @param state armed = 已经按过第一下、在等第二下;busy = 停止请求已经在飞。
560
570
  * @returns busy 优先(在飞时按不动),其次 armed,否则 idle。
@@ -2767,7 +2777,8 @@ var dictionaries = {
2767
2777
  "env.stepFailed.shell": "执行通道不可用",
2768
2778
  "env.bridge.ok": "已连接",
2769
2779
  "env.bridge.short": "没连上",
2770
- "env.bridge.why": "面板的数据要走你浏览器里的登录态:这个通道没连上,模块就取不到数据(不拦路,但先修好它)。",
2780
+ "env.bridge.why": "面板的数据要走你浏览器里的登录态:这个通道没连上,模块就取不到数据(不拦路:修好它,或者先进入工作台)。",
2781
+ "env.bridge.hold": "浏览器通道还没连上 —— 按下面修好后点「重试」,也可以先进工作台。",
2771
2782
  "env.bridge.daemon": "后台服务没起来:正常它会自己起,先在终端跑下面这条命令。",
2772
2783
  "env.bridge.extension": "浏览器扩展没连上:在 Chrome 应用商店装一次就好(装完回来点「重试」)。已经装过的话,先跑下面这条命令。",
2773
2784
  "env.bridge.extensionLink": "去 Chrome 应用商店装扩展",
@@ -2785,11 +2796,21 @@ var dictionaries = {
2785
2796
  "env.fix.network": "网络不通:检查网络或代理设置后重试。",
2786
2797
  "env.fix.details": "详细输出(排障用)",
2787
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": "先进入工作台",
2788
2808
  "env.copyCommand": "复制命令",
2789
2809
  "env.copied": "已复制",
2790
2810
  "env.step.opencli": "检测工作台的 opencli",
2791
2811
  "env.step.codingPlugin": "检测 coding 插件",
2792
2812
  "env.step.bridge": "检测浏览器通道",
2813
+ "env.step.diy": "检查 DIY 模式",
2793
2814
  "env.step.warm": "预热站点登录态",
2794
2815
  "env.warm.done": "已预热 {n} 个站点",
2795
2816
  "env.warm.short": "这次没热上 —— 不影响用,面板第一次读取会自己补",
@@ -3211,7 +3232,8 @@ var dictionaries = {
3211
3232
  "env.stepFailed.shell": "no shell channel",
3212
3233
  "env.bridge.ok": "Connected",
3213
3234
  "env.bridge.short": "Not connected",
3214
- "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.",
3215
3237
  "env.bridge.daemon": "The background service is not running. It usually starts by itself — run the command below in your terminal first.",
3216
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.",
3217
3239
  "env.bridge.extensionLink": "Get the extension in the Chrome Web Store",
@@ -3229,11 +3251,21 @@ var dictionaries = {
3229
3251
  "env.fix.network": "Network unreachable: check your network or proxy, then retry.",
3230
3252
  "env.fix.details": "Details (for troubleshooting)",
3231
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",
3232
3263
  "env.copyCommand": "Copy command",
3233
3264
  "env.copied": "Copied",
3234
3265
  "env.step.opencli": "Checking the workbench opencli",
3235
3266
  "env.step.codingPlugin": "Checking the coding plugin",
3236
3267
  "env.step.bridge": "Checking the browser bridge",
3268
+ "env.step.diy": "Checking DIY mode",
3237
3269
  "env.step.warm": "Warming up site sessions",
3238
3270
  "env.warm.done": "{n} site(s) warmed up",
3239
3271
  "env.warm.short": "Not warmed this time — harmless; the panel heals on first read",
@@ -7785,6 +7817,10 @@ function stepDetailText(step, snapshot, t) {
7785
7817
  if (step.status === "ok") return t("env.bridge.ok");
7786
7818
  if (step.status === "warn") return t("env.bridge.short");
7787
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
+ }
7788
7824
  if (step.id === "warm") {
7789
7825
  if (step.status === "ok") return t("env.warm.done", { n: step.detail ?? "0" });
7790
7826
  if (step.status === "warn") return t("env.warm.short");
@@ -7880,6 +7916,7 @@ var GIVE_UP_MS = 12e4;
7880
7916
  function stepLabel$1(id, t) {
7881
7917
  if (id === "opencli") return t("env.step.opencli");
7882
7918
  if (id === "warm") return t("env.step.warm");
7919
+ if (id === "diy") return t("env.step.diy");
7883
7920
  return t("env.step.bridge");
7884
7921
  }
7885
7922
  function stepMark(step) {
@@ -7907,6 +7944,18 @@ function createForcePlan() {
7907
7944
  }
7908
7945
  };
7909
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
+ }
7910
7959
  function commandRow(t) {
7911
7960
  const row = el("div", BootGate_module_default.commandRow);
7912
7961
  const code = el("code", BootGate_module_default.command);
@@ -7979,13 +8028,32 @@ function createBootGate(ctx, options = {}) {
7979
8028
  const bridgeCommand = commandRow(t);
7980
8029
  const bridgeRetry = el("button", BootGate_module_default.button, t("env.retry"));
7981
8030
  bridgeRetry.setAttribute("type", "button");
8031
+ const bridgeSkip = el("button", BootGate_module_default.button, t("env.bridge.continue"));
8032
+ bridgeSkip.setAttribute("type", "button");
7982
8033
  const bridgeButtons = el("div", BootGate_module_default.buttons);
7983
- bridgeButtons.append(bridgeRetry);
8034
+ bridgeButtons.append(bridgeRetry, bridgeSkip);
7984
8035
  const bridgeRaw = el("details", BootGate_module_default.raw);
7985
8036
  const bridgeRawText = el("pre", BootGate_module_default.error);
7986
8037
  bridgeRaw.append(el("summary", BootGate_module_default.rawSummary, t("env.fix.details")), bridgeRawText);
7987
8038
  bridge.append(bridgeTitle, bridgeWhy, bridgeLines, bridgeCommand.row, bridgeButtons, bridgeRaw);
7988
- 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);
7989
8057
  overlay.append(card);
7990
8058
  document.body.append(overlay);
7991
8059
  const previousInert = root?.inert ?? false;
@@ -8043,7 +8111,7 @@ function createBootGate(ctx, options = {}) {
8043
8111
  return row;
8044
8112
  }
8045
8113
  function render(snapshot) {
8046
- 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");
8047
8115
  const items = snapshot.steps.map((step) => {
8048
8116
  const row = ensureRow(step);
8049
8117
  row.item.dataset.status = step.status;
@@ -8101,6 +8169,15 @@ function createBootGate(ctx, options = {}) {
8101
8169
  bridgeRawText.textContent = bridgeStep.detail ?? "";
8102
8170
  bridgeRaw.hidden = (bridgeStep.detail ?? "") === "";
8103
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
+ }
8104
8181
  }
8105
8182
  async function poll() {
8106
8183
  if (cancelled) return;
@@ -8112,19 +8189,22 @@ function createBootGate(ctx, options = {}) {
8112
8189
  return;
8113
8190
  }
8114
8191
  render(response);
8115
- if (response.phase === "ready") {
8192
+ const action = gateActionOf(response);
8193
+ if (action === "close") {
8116
8194
  close();
8117
8195
  return;
8118
8196
  }
8119
- if (response.phase === "failed") return;
8197
+ if (action === "hold") return;
8120
8198
  timer = setTimeout(() => void poll(), POLL_MS);
8121
8199
  }
8122
8200
  function retryNow() {
8123
8201
  forcePlan.again();
8124
8202
  actions.hidden = true;
8125
8203
  bridge.hidden = true;
8204
+ diy.hidden = true;
8126
8205
  retry.setAttribute("disabled", "");
8127
8206
  bridgeRetry.setAttribute("disabled", "");
8207
+ diyRetry.setAttribute("disabled", "");
8128
8208
  hint.textContent = t("env.checking");
8129
8209
  if (pinned !== null) {
8130
8210
  render(pinned);
@@ -8134,6 +8214,9 @@ function createBootGate(ctx, options = {}) {
8134
8214
  }
8135
8215
  retry.addEventListener("click", retryNow);
8136
8216
  bridgeRetry.addEventListener("click", retryNow);
8217
+ bridgeSkip.addEventListener("click", close);
8218
+ diyRetry.addEventListener("click", retryNow);
8219
+ diySkip.addEventListener("click", close);
8137
8220
  const search = typeof window === "undefined" ? "" : window.location?.search ?? "";
8138
8221
  if (devSurface.on(search)) {
8139
8222
  const strip = devSurface.gateStrip({ onPick: (snapshot) => {
@@ -10833,17 +10916,19 @@ function useLongTask(options) {
10833
10916
  setStopping((current) => current === id ? null : current);
10834
10917
  }
10835
10918
  }, [send]);
10836
- const every = longTaskPollMs(runs.some((run) => longTaskDot(run.status) === "ongoing"), options.pollMs);
10919
+ const every = longTaskPollMs(runs.some((run) => longTaskIsLive(run.status)), options.pollMs);
10837
10920
  useEffect(() => {
10838
10921
  if (!enabled) return void 0;
10839
10922
  const timer = setInterval(() => setTick((count) => count + 1), every);
10840
10923
  return () => clearInterval(timer);
10841
10924
  }, [enabled, every]);
10925
+ const refresh = useCallback(() => setTick((count) => count + 1), []);
10842
10926
  return {
10843
10927
  runs,
10844
10928
  stop,
10845
10929
  stopping,
10846
- stopFailed: write.failed
10930
+ stopFailed: write.failed,
10931
+ refresh
10847
10932
  };
10848
10933
  }
10849
10934
  //#endregion
@@ -11099,6 +11184,7 @@ var kit = {
11099
11184
  ...context_chips_exports
11100
11185
  },
11101
11186
  lib: {
11187
+ longTaskIsLive,
11102
11188
  MIN_BUSY_MS: 500,
11103
11189
  blockerOfCapability,
11104
11190
  blockerOfError,
@@ -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,
@@ -9,7 +9,7 @@ import * as capabilityGateModule from '../shared/hooks/use-capability-gate';
9
9
  import * as unitBoardModule from '../shared/hooks/use-unit-board';
10
10
  import * as visibleBusyModule from '../shared/hooks/use-visible-busy';
11
11
  import * as blockerStateModule from '../shared/components/BlockerState';
12
- import { ActivityAction, ActivityBlock, ActivityLine, ApprovalDetail, ApprovalPlan, Card, CardAnchor, CardBoundary, CardHead, CenteredState, ChoiceGroup, ConfirmBar, EmptyState, Field, Form, FormActions, FormRow, FormRows, IconBrandMark, IconConversation, IconModuleMark, IconNpm, IconOpencli, IconPanelScreen, Link, LongTaskRow, Note, Row, Rows, SaveBar, SelectField, SettingsSection, SkeletonRows, Spinner, Stack, StateBlock, TextField, apiGet, apiPost, blockerOfCapability, blockerOfError, cardIdOf, cx, defaultSelectFilter, duplicateKeysOf, errorReasonOf, excerpt, filterSelectOptions, firstProblemOf, formFirstProblemOf, formValuesOf, holdBusy, parseJsonLoose, parseLocalTimestamp, readText, relativeTime, relativeTimeAgo, relativeTimeBucket, relativeTimeCompact, revealFirstProblem, searchTextOf, selectCursorValue, selectEntries, shortenPath, stepSelectCursor, writeText } from '@dsh-workbench/shared';
12
+ import { ActivityAction, ActivityBlock, ActivityLine, ApprovalDetail, ApprovalPlan, Card, CardAnchor, CardBoundary, CardHead, CenteredState, ChoiceGroup, ConfirmBar, EmptyState, Field, Form, FormActions, FormRow, FormRows, IconBrandMark, IconConversation, IconModuleMark, IconNpm, IconOpencli, IconPanelScreen, Link, LongTaskRow, longTaskIsLive, Note, Row, Rows, SaveBar, SelectField, SettingsSection, SkeletonRows, Spinner, Stack, StateBlock, TextField, apiGet, apiPost, blockerOfCapability, blockerOfError, cardIdOf, cx, defaultSelectFilter, duplicateKeysOf, errorReasonOf, excerpt, filterSelectOptions, firstProblemOf, formFirstProblemOf, formValuesOf, holdBusy, parseJsonLoose, parseLocalTimestamp, readText, relativeTime, relativeTimeAgo, relativeTimeBucket, relativeTimeCompact, revealFirstProblem, searchTextOf, selectCursorValue, selectEntries, shortenPath, stepSelectCursor, writeText } from '@dsh-workbench/shared';
13
13
  export declare const kit: {
14
14
  hooks: {
15
15
  heldPhase(phase: visibleBusyModule.HeldPhase, refreshing: boolean): visibleBusyModule.HeldPhase;
@@ -102,6 +102,7 @@ export declare const kit: {
102
102
  apiPost: typeof apiPost;
103
103
  };
104
104
  lib: {
105
+ longTaskIsLive: typeof longTaskIsLive;
105
106
  MIN_BUSY_MS: number;
106
107
  blockerOfCapability: typeof blockerOfCapability;
107
108
  blockerOfError: typeof blockerOfError;
@@ -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;
@@ -3,12 +3,15 @@ import { type LongTaskStatus, type WriteOutcome } from '@dsh-workbench/shared';
3
3
  * 这一单元的长任务清单与停止口:`runs` 是这次读交回的那一批(读数还没回来时是空数组);
4
4
  * `stop(id)` 请外壳停某一趟 —— 回执没成就带回一句人话(调用点自己决定说不说);
5
5
  * `stopping` 是正在请求停止的那一趟 id(没有就是 `null`),`stopFailed` 是上一次停止请求失败那句话(没有就是 `null`)。
6
+ * `refresh()` 让这一格**立刻**重读一次(不必等下一拍轮询)—— 自己写完/发起完之后用它,
7
+ * 别去拼读口路由、也别为一个读数去碰事实通道。
6
8
  */
7
9
  export interface LongTaskFeed<T> {
8
10
  readonly runs: readonly T[];
9
11
  stop(id: string): Promise<WriteOutcome<unknown>>;
10
12
  readonly stopping: string | null;
11
13
  readonly stopFailed: string | null;
14
+ refresh(): void;
12
15
  }
13
16
  /**
14
17
  * 卡片要轮询长任务那一行时调用:有在跑的(`running` / `needs-login`)按 `longTaskPollMs` 走快档、空闲走慢档;