@dsh-workbench/core 0.1.18 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/client.js +2241 -955
  2. package/dist/client/client.body.js +2241 -955
  3. package/dist/client/index.js +2242 -956
  4. package/dist/host/index.js +1840 -332
  5. package/dist/types/app/client.d.ts +225 -81
  6. package/dist/types/app/layouts/__tests__/async-feedback.test.d.ts +1 -0
  7. package/dist/types/app/layouts/market/__tests__/add-module-stages.test.d.ts +1 -0
  8. package/dist/types/app/layouts/market/add-module-text.d.ts +16 -1
  9. package/dist/types/app/layouts/market/use-add-module.d.ts +3 -8
  10. package/dist/types/app/layouts/market/use-module-versions.d.ts +2 -1
  11. package/dist/types/app/layouts/rightbar/MarketView.d.ts +7 -1
  12. package/dist/types/app/layouts/rightbar/ModulesPage.d.ts +4 -2
  13. package/dist/types/app/layouts/rightbar/PanelGroupRows.d.ts +10 -0
  14. package/dist/types/app/layouts/rightbar/PanelModules.d.ts +2 -2
  15. package/dist/types/app/layouts/rightbar/PanelRowControls.d.ts +13 -0
  16. package/dist/types/app/layouts/rightbar/__tests__/market-catalog.test.d.ts +1 -0
  17. package/dist/types/app/layouts/rightbar/__tests__/market-dots.test.d.ts +1 -0
  18. package/dist/types/app/layouts/rightbar/__tests__/market-row-icon.test.d.ts +1 -0
  19. package/dist/types/app/layouts/rightbar/__tests__/market-section-icon.test.d.ts +1 -0
  20. package/dist/types/app/layouts/rightbar/__tests__/market-state-rules.test.d.ts +1 -0
  21. package/dist/types/app/layouts/rightbar/__tests__/market-workbench.test.d.ts +1 -0
  22. package/dist/types/app/layouts/rightbar/__tests__/panel-dismiss.test.d.ts +1 -0
  23. package/dist/types/app/layouts/rightbar/__tests__/panel-drag-target.test.d.ts +1 -0
  24. package/dist/types/app/layouts/rightbar/__tests__/panel-plugin-rows.test.d.ts +1 -0
  25. package/dist/types/app/layouts/rightbar/__tests__/panel-scope-ui.test.d.ts +1 -0
  26. package/dist/types/app/layouts/rightbar/__tests__/panel-write.test.d.ts +1 -0
  27. package/dist/types/app/layouts/rightbar/board-selection.d.ts +20 -0
  28. package/dist/types/app/layouts/rightbar/dsh-borrow.d.ts +15 -0
  29. package/dist/types/app/layouts/rightbar/dsh-borrow.test.d.ts +1 -0
  30. package/dist/types/app/layouts/rightbar/dsh-surface.d.ts +11 -0
  31. package/dist/types/app/layouts/rightbar/dsh-surface.test.d.ts +1 -0
  32. package/dist/types/app/layouts/rightbar/market-act.d.ts +21 -0
  33. package/dist/types/app/layouts/rightbar/market-installed-mock.d.ts +9 -0
  34. package/dist/types/app/layouts/rightbar/market-mock-types.d.ts +58 -0
  35. package/dist/types/app/layouts/rightbar/market-mock.d.ts +2 -43
  36. package/dist/types/app/layouts/rightbar/market-plugin-rows.d.ts +3 -1
  37. package/dist/types/app/layouts/rightbar/market-row-icon.d.ts +7 -0
  38. package/dist/types/app/layouts/rightbar/market-row-types.d.ts +37 -0
  39. package/dist/types/app/layouts/rightbar/market-rows.d.ts +13 -53
  40. package/dist/types/app/layouts/rightbar/market-section-icon.d.ts +3 -0
  41. package/dist/types/app/layouts/rightbar/market-section.d.ts +18 -0
  42. package/dist/types/app/layouts/rightbar/market-shared-rows.d.ts +21 -0
  43. package/dist/types/app/layouts/rightbar/market-view-text.d.ts +7 -1
  44. package/dist/types/app/layouts/rightbar/market-workbench-row.d.ts +5 -0
  45. package/dist/types/app/layouts/rightbar/panel-dismiss.d.ts +2 -0
  46. package/dist/types/app/layouts/rightbar/panel-mock.d.ts +3 -2
  47. package/dist/types/app/layouts/rightbar/panel-write.d.ts +15 -0
  48. package/dist/types/app/layouts/rightbar/use-drag-listeners.d.ts +6 -0
  49. package/dist/types/app/layouts/rightbar/use-panels.d.ts +15 -1
  50. package/dist/types/app/layouts/sidebar/__tests__/session-row-ui.test.d.ts +1 -0
  51. package/dist/types/app/units/registry.d.ts +5 -2
  52. package/dist/types/modules/panels/__tests__/diy-harness.d.ts +19 -0
  53. package/dist/types/modules/panels/__tests__/diy-report.test.d.ts +1 -0
  54. package/dist/types/modules/panels/__tests__/diy-tools-health.test.d.ts +1 -0
  55. package/dist/types/modules/panels/__tests__/diy-tools-panel.test.d.ts +1 -0
  56. package/dist/types/modules/panels/__tests__/plugin-panel-market-parity.test.d.ts +1 -0
  57. package/dist/types/modules/panels/__tests__/plugin-panel.test.d.ts +1 -0
  58. package/dist/types/modules/panels/__tests__/plugin-tool-units.test.d.ts +1 -0
  59. package/dist/types/modules/panels/__tests__/scope-machine.test.d.ts +1 -0
  60. package/dist/types/modules/panels/__tests__/scope-support.d.ts +51 -0
  61. package/dist/types/modules/panels/model/config.d.ts +23 -3
  62. package/dist/types/modules/panels/model/diy-report.d.ts +63 -0
  63. package/dist/types/modules/panels/model/market-dispatch.d.ts +4 -0
  64. package/dist/types/modules/panels/model/market-registry.d.ts +12 -0
  65. package/dist/types/modules/panels/model/market-registry.test.d.ts +1 -0
  66. package/dist/types/modules/panels/model/market-types.d.ts +1 -0
  67. package/dist/types/modules/panels/model/market.d.ts +3 -2
  68. package/dist/types/modules/panels/model/plugin-panel.d.ts +3 -0
  69. package/dist/types/modules/panels/model/prefs.d.ts +3 -1
  70. package/dist/types/modules/panels/model/service.d.ts +5 -1
  71. package/dist/types/shared/api/bundle-install.d.ts +15 -1
  72. package/dist/types/shared/api/host/__tests__/approval-note-injection.test.d.ts +1 -0
  73. package/dist/types/shared/api/host/__tests__/panel-state-route.test.d.ts +1 -0
  74. package/dist/types/shared/api/host/__tests__/plugin-catalog.test.d.ts +1 -0
  75. package/dist/types/shared/api/host/__tests__/plugin-login-hint.test.d.ts +1 -0
  76. package/dist/types/shared/api/host/__tests__/plugin-tools.test.d.ts +1 -0
  77. package/dist/types/shared/api/host/bundle-inspect-kind.test.d.ts +1 -0
  78. package/dist/types/shared/api/host/bundle-routes-self.test.d.ts +1 -0
  79. package/dist/types/shared/api/host/installed-module.d.ts +12 -0
  80. package/dist/types/shared/api/host/mount-units.d.ts +1 -0
  81. package/dist/types/shared/api/host/plugin-catalog.d.ts +60 -0
  82. package/dist/types/shared/api/host/plugin-login-hint.d.ts +2 -0
  83. package/dist/types/shared/api/host/plugin-tools.d.ts +10 -0
  84. package/dist/types/shared/api/host/plugin-units.d.ts +47 -0
  85. package/dist/types/shared/api/host/unit-registry.d.ts +9 -0
  86. package/dist/types/shared/api/unit-manifests.d.ts +1 -0
  87. package/dist/types/shared/components/PanelFoot.d.ts +1 -1
  88. package/dist/types/shared/hooks/panel-state-report.d.ts +13 -0
  89. package/dist/types/shared/hooks/panel-state-report.test.d.ts +1 -0
  90. package/docs/HANDOFF.md +7 -0
  91. package/package.json +2 -2
  92. package/preset/diy.patch.yml +5 -26
  93. package/skills/workbench-module-diy/REVIEW.md +22 -0
  94. package/skills/workbench-module-diy/SKILL.md +3 -1
  95. package/skills/workbench-module-diy/TOOLING.md +2 -0
@@ -11,9 +11,11 @@ var PREFS_PREFIX = `${API_PREFIX}/prefs`;
11
11
  var EVENTS_PATH = "/workbench/events";
12
12
  var ACTIONS_PREFIX = `${API_PREFIX}/actions`;
13
13
  var PROVISION_PREFIX = `${API_PREFIX}/provision`;
14
+ var PANEL_STATE_PREFIX = `${API_PREFIX}/panel-state`;
14
15
  var OPENCLI_PIN = "1.8.8";
15
16
  var PANELS_SERVICE = "workbenchPanels";
16
17
  var UNITS_SERVICE = "workbenchUnits";
18
+ var PANEL_STATE_SERVICE = "workbenchPanelState";
17
19
  var OPENCLI_TIMEOUT_MS = 12e4;
18
20
  var WARM_TIMEOUT_MS = 2e4;
19
21
  var INSTALL_TIMEOUT_MS = 3e5;
@@ -50,6 +52,11 @@ function parseMarkers(text) {
50
52
  }
51
53
  return refs;
52
54
  }
55
+ //#endregion
56
+ //#region ../shared/dist/api/unit.js
57
+ function hasEnvSupply(manifest) {
58
+ return (manifest.env?.plugins ?? []).length > 0 || (manifest.env?.libs ?? []).length > 0;
59
+ }
53
60
  var SUPPORTED_API_VERSIONS = [1];
54
61
  var DECLARATION_KEYS = [
55
62
  "apiVersion",
@@ -212,6 +219,267 @@ function parseJsonLoose(text) {
212
219
  }
213
220
  }
214
221
  }
222
+ var PANEL_STATE_MAX_TEXT = 2e3;
223
+ var PHASES = [
224
+ "idle",
225
+ "loading",
226
+ "ready",
227
+ "error"
228
+ ];
229
+ function asRecord$10(value) {
230
+ return typeof value === "object" && value !== null ? value : void 0;
231
+ }
232
+ function textOf$6(value, max) {
233
+ if (typeof value !== "string") return "";
234
+ const trimmed = value.trim();
235
+ return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
236
+ }
237
+ function atOf(value) {
238
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
239
+ if (value < 0) return null;
240
+ return Math.trunc(value);
241
+ }
242
+ function errorOf(raw) {
243
+ const record = asRecord$10(raw);
244
+ if (record === void 0) return void 0;
245
+ const code = textOf$6(record["code"], 120);
246
+ const message = textOf$6(record["message"], PANEL_STATE_MAX_TEXT);
247
+ if (code === "" && message === "") return void 0;
248
+ return {
249
+ code: code === "" ? "unknown" : code,
250
+ message
251
+ };
252
+ }
253
+ function parseCardsReport(payload) {
254
+ const record = asRecord$10(payload);
255
+ if (record === void 0) return {
256
+ ok: false,
257
+ message: "请求体要是一个对象"
258
+ };
259
+ const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
260
+ if (workspace === "") return {
261
+ ok: false,
262
+ message: "workspace 不能是空的 —— 面板按工作区存"
263
+ };
264
+ const raw = record["cards"];
265
+ if (!Array.isArray(raw)) return {
266
+ ok: false,
267
+ message: "cards 要是一个数组"
268
+ };
269
+ const cards = [];
270
+ for (const item of raw.slice(0, 32)) {
271
+ const card = asRecord$10(item);
272
+ if (card === void 0) continue;
273
+ const unit = textOf$6(card["unit"], 200);
274
+ const route = textOf$6(card["route"], 400);
275
+ const phase = textOf$6(card["phase"], 20);
276
+ const at = atOf(card["at"]);
277
+ if (unit === "" || route === "" || at === null || !PHASES.includes(phase)) continue;
278
+ const error = errorOf(card["error"]);
279
+ cards.push({
280
+ unit,
281
+ route,
282
+ phase,
283
+ at,
284
+ ...error === void 0 ? {} : { error }
285
+ });
286
+ }
287
+ if (cards.length === 0) return {
288
+ ok: false,
289
+ message: "cards 里没有一条能认的(unit / route / phase / at 都要有)"
290
+ };
291
+ return {
292
+ ok: true,
293
+ value: {
294
+ workspace,
295
+ cards
296
+ }
297
+ };
298
+ }
299
+ function parseCrashReport(payload) {
300
+ const record = asRecord$10(payload);
301
+ if (record === void 0) return {
302
+ ok: false,
303
+ message: "请求体要是一个对象"
304
+ };
305
+ const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
306
+ const unit = textOf$6(record["unit"], 200);
307
+ const reason = textOf$6(record["reason"], PANEL_STATE_MAX_TEXT);
308
+ if (workspace === "") return {
309
+ ok: false,
310
+ message: "workspace 不能是空的"
311
+ };
312
+ if (unit === "") return {
313
+ ok: false,
314
+ message: "unit 不能是空的"
315
+ };
316
+ if (reason === "") return {
317
+ ok: false,
318
+ message: "reason 不能是空的(没有原因的崩溃没法查)"
319
+ };
320
+ return {
321
+ ok: true,
322
+ value: {
323
+ workspace,
324
+ unit,
325
+ reason,
326
+ at: atOf(record["at"]) ?? Date.now()
327
+ }
328
+ };
329
+ }
330
+ function createPanelStateStore() {
331
+ const workspaces = /* @__PURE__ */ new Map();
332
+ function trim(entry, max) {
333
+ while (entry.size > max) {
334
+ const oldest = entry.keys().next();
335
+ if (oldest.done === true) return;
336
+ entry.delete(oldest.value);
337
+ }
338
+ }
339
+ function workspaceEntryOf(workspace) {
340
+ const existing = workspaces.get(workspace);
341
+ if (existing !== void 0) return existing;
342
+ const created = {
343
+ at: 0,
344
+ units: /* @__PURE__ */ new Map()
345
+ };
346
+ workspaces.set(workspace, created);
347
+ trim(workspaces, 64);
348
+ return created;
349
+ }
350
+ function unitEntryOf(entry, unit) {
351
+ const existing = entry.units.get(unit);
352
+ if (existing !== void 0) return existing;
353
+ const created = {
354
+ at: 0,
355
+ cards: /* @__PURE__ */ new Map()
356
+ };
357
+ entry.units.set(unit, created);
358
+ return created;
359
+ }
360
+ return {
361
+ cards(report) {
362
+ const entry = workspaceEntryOf(report.workspace);
363
+ for (const card of report.cards) {
364
+ const unit = unitEntryOf(entry, card.unit);
365
+ unit.cards.delete(card.route);
366
+ unit.cards.set(card.route, card);
367
+ trim(unit.cards, 12);
368
+ if (card.at > unit.at) unit.at = card.at;
369
+ if (card.at > entry.at) entry.at = card.at;
370
+ }
371
+ },
372
+ crash(report) {
373
+ const entry = workspaceEntryOf(report.workspace);
374
+ const unit = unitEntryOf(entry, report.unit);
375
+ const previous = unit.crash;
376
+ const counted = previous !== void 0 && previous.reason === report.reason && report.at >= previous.at && report.at - previous.at <= 6e5;
377
+ unit.crash = {
378
+ unit: report.unit,
379
+ reason: report.reason,
380
+ at: report.at,
381
+ count: counted ? previous.count + 1 : 1
382
+ };
383
+ if (report.at > unit.at) unit.at = report.at;
384
+ if (report.at > entry.at) entry.at = report.at;
385
+ },
386
+ snapshot(workspace) {
387
+ if (workspace === null) return {
388
+ workspace: null,
389
+ at: 0,
390
+ units: []
391
+ };
392
+ const entry = workspaces.get(workspace);
393
+ if (entry === void 0) return {
394
+ workspace,
395
+ at: 0,
396
+ units: []
397
+ };
398
+ const units = [...entry.units.entries()].map(([unit, one]) => {
399
+ const cards = [...one.cards.values()].sort((a, b) => a.route.localeCompare(b.route));
400
+ const okAt = cards.reduce((best, card) => card.phase === "ready" && (best === null || card.at > best) ? card.at : best, null);
401
+ return {
402
+ unit,
403
+ at: one.at,
404
+ okAt,
405
+ cards,
406
+ ...one.crash === void 0 ? {} : { crash: one.crash }
407
+ };
408
+ });
409
+ units.sort((a, b) => b.at - a.at || a.unit.localeCompare(b.unit));
410
+ return {
411
+ workspace,
412
+ at: entry.at,
413
+ units
414
+ };
415
+ }
416
+ };
417
+ }
418
+ //#endregion
419
+ //#region ../shared/dist/host/cli-error.js
420
+ function unquote(raw) {
421
+ const value = raw.trim();
422
+ if (value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1);
423
+ return value;
424
+ }
425
+ function joinBlock(previous, line, fold) {
426
+ if (previous === "") return line;
427
+ return fold ? `${previous} ${line}` : `${previous}\n${line}`;
428
+ }
429
+ function parseCliErrorEnvelope(text) {
430
+ if (text === void 0 || text.trim() === "") return void 0;
431
+ const lines = text.split("\n");
432
+ const start = lines.findIndex((line) => /^error:\s*$/.test(line.trim()) || /^error:\s*\S/.test(line.trim()));
433
+ if (start === -1) return void 0;
434
+ const scalars = {};
435
+ let current;
436
+ let block;
437
+ const errorIndent = lines[start]?.search(/\S/) ?? 0;
438
+ for (const line of lines.slice(start + 1)) {
439
+ if (line.trim() === "") continue;
440
+ const indent = line.search(/\S/);
441
+ if (block !== void 0 && indent > block.indent) {
442
+ scalars[block.key] = joinBlock(scalars[block.key] ?? "", line.trim(), block.fold);
443
+ continue;
444
+ }
445
+ block = void 0;
446
+ if (indent <= errorIndent) break;
447
+ const match = /^\s*(code|message|help|exitCode):\s*(.*)$/.exec(line);
448
+ if (match === null) {
449
+ if (current !== void 0) scalars[current] = `${scalars[current] ?? ""} ${line.trim()}`.trim();
450
+ continue;
451
+ }
452
+ const [, key, value] = match;
453
+ if (key === void 0) continue;
454
+ const raw = (value ?? "").trim();
455
+ const marker = /^([|>])[0-9]*([+-]?)$/.exec(raw);
456
+ if (marker !== null) {
457
+ scalars[key] = "";
458
+ block = {
459
+ key,
460
+ fold: marker[1] === ">",
461
+ indent
462
+ };
463
+ current = key;
464
+ continue;
465
+ }
466
+ scalars[key] = unquote(raw);
467
+ current = key;
468
+ }
469
+ const code = scalars["code"];
470
+ const message = scalars["message"];
471
+ if (code === void 0 || code === "") return void 0;
472
+ const exitCode = scalars["exitCode"];
473
+ return {
474
+ code,
475
+ message: message ?? code,
476
+ ...scalars["help"] === void 0 || scalars["help"] === "" ? {} : { help: scalars["help"] },
477
+ ...exitCode === void 0 || Number.isNaN(Number(exitCode)) ? {} : { exitCode: Number(exitCode) }
478
+ };
479
+ }
480
+ function helpUrlOf(envelope) {
481
+ return /https?:\/\/[^\s"'`]+/.exec(envelope.help ?? "")?.[0];
482
+ }
215
483
  //#endregion
216
484
  //#region ../shared/dist/host/data-root.js
217
485
  function dshHome() {
@@ -282,20 +550,20 @@ var PERMISSION_CODES = /* @__PURE__ */ new Set([
282
550
  function asRecord$9(value) {
283
551
  return typeof value === "object" && value !== null ? value : void 0;
284
552
  }
285
- function textOf$4(value) {
553
+ function textOf$5(value) {
286
554
  return typeof value === "string" ? value.trim() : "";
287
555
  }
288
556
  function codeOf(page) {
289
557
  const record = asRecord$9(page);
290
- return record === void 0 ? "" : textOf$4(record["code"]);
558
+ return record === void 0 ? "" : textOf$5(record["code"]);
291
559
  }
292
- function messageOf$1(page) {
560
+ function messageOf$2(page) {
293
561
  const record = asRecord$9(page);
294
- return record === void 0 ? "" : textOf$4(record["message"]);
562
+ return record === void 0 ? "" : textOf$5(record["message"]);
295
563
  }
296
564
  function hintOf$1(page) {
297
565
  const record = asRecord$9(page);
298
- const hint = record === void 0 ? "" : textOf$4(record["hint"]);
566
+ const hint = record === void 0 ? "" : textOf$5(record["hint"]);
299
567
  return hint === "" ? void 0 : hint;
300
568
  }
301
569
  function pageFailed(page) {
@@ -484,7 +752,7 @@ async function browserRead(options) {
484
752
  message: "读到了"
485
753
  };
486
754
  const pageCode = codeOf(page) || "read-failed";
487
- const message = messageOf$1(page) || `读不到(${pageCode})`;
755
+ const message = messageOf$2(page) || `读不到(${pageCode})`;
488
756
  const blocker = blockerFor(pageCode);
489
757
  if (blocker === "auth-required") return {
490
758
  ok: false,
@@ -526,7 +794,7 @@ var DEFAULT_TEXT_MAX = 200;
526
794
  function asRecord$8(value) {
527
795
  return typeof value === "object" && value !== null ? value : void 0;
528
796
  }
529
- function textOf$3(value) {
797
+ function textOf$4(value) {
530
798
  return typeof value === "string" ? value.trim() : "";
531
799
  }
532
800
  function entriesOf(value) {
@@ -539,8 +807,8 @@ function entriesOf(value) {
539
807
  if (item === void 0) continue;
540
808
  const nth = typeof item["nth"] === "number" ? item["nth"] : entries.length;
541
809
  entries.push({
542
- text: textOf$3(item["text"]),
543
- tag: textOf$3(item["tag"]),
810
+ text: textOf$4(item["text"]),
811
+ tag: textOf$4(item["tag"]),
544
812
  nth
545
813
  });
546
814
  }
@@ -549,7 +817,7 @@ function entriesOf(value) {
549
817
  function reasonOf(stdout, stderr) {
550
818
  const parsed = asRecord$8(parseJsonLoose(stdout));
551
819
  const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
552
- const message = error === void 0 ? "" : textOf$3(error["message"]);
820
+ const message = error === void 0 ? "" : textOf$4(error["message"]);
553
821
  if (message !== "") return message;
554
822
  const lines = (stderr.trim() === "" ? stdout : stderr).split("\n").map((one) => one.trim()).filter((one) => one !== "");
555
823
  return (lines[lines.length - 1] ?? "").slice(-200);
@@ -557,7 +825,7 @@ function reasonOf(stdout, stderr) {
557
825
  function hintOf(stdout) {
558
826
  const parsed = asRecord$8(parseJsonLoose(stdout));
559
827
  const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
560
- const hint = error === void 0 ? "" : textOf$3(error["hint"]);
828
+ const hint = error === void 0 ? "" : textOf$4(error["hint"]);
561
829
  return hint === "" ? void 0 : hint;
562
830
  }
563
831
  function sameOrigin(left, right) {
@@ -571,7 +839,7 @@ function urlOf(stdout) {
571
839
  const bare = (stdout.split("\n").map((one) => one.trim()).filter((one) => one !== "")[0] ?? "").replace(/^url:\s*/i, "").trim().replace(/^["']+|["']+$/g, "");
572
840
  if (bare.startsWith("{")) {
573
841
  const parsed = asRecord$8(parseJsonLoose(bare));
574
- const url = parsed === void 0 ? "" : textOf$3(parsed["url"]);
842
+ const url = parsed === void 0 ? "" : textOf$4(parsed["url"]);
575
843
  if (url !== "") return url;
576
844
  }
577
845
  return bare;
@@ -796,7 +1064,8 @@ var PROVISION_ACTIONS = [
796
1064
  "cancel-install",
797
1065
  "update-dependency",
798
1066
  "update-plugin",
799
- "uninstall-plugin"
1067
+ "uninstall-plugin",
1068
+ "update-workbench"
800
1069
  ];
801
1070
  var DEPENDENCY_ACTIONS = ["update-dependency"];
802
1071
  function isDependencyAction(action) {
@@ -807,6 +1076,7 @@ function provisionTargetOf(action) {
807
1076
  if (action === "install-bundle") return "spec";
808
1077
  if (action === "remove-bundle") return "name";
809
1078
  if (action === "update-plugin" || action === "uninstall-plugin") return "name";
1079
+ if (action === "update-workbench") return "name";
810
1080
  if (action === "cancel-install") return "requestId";
811
1081
  return "unit";
812
1082
  }
@@ -820,7 +1090,7 @@ var TARGET_CODE = {
820
1090
  function asRecord$7(value) {
821
1091
  return typeof value === "object" && value !== null ? value : {};
822
1092
  }
823
- function stringsOf$2(value) {
1093
+ function stringsOf$3(value) {
824
1094
  if (!Array.isArray(value)) return void 0;
825
1095
  const items = value.filter((item) => typeof item === "string" && item.trim() !== "");
826
1096
  return items.length === 0 ? void 0 : items;
@@ -842,6 +1112,10 @@ function requestOf$1(action, value) {
842
1112
  action,
843
1113
  name: value
844
1114
  };
1115
+ if (action === "update-workbench") return {
1116
+ action,
1117
+ name: value
1118
+ };
845
1119
  if (action === "cancel-install") return {
846
1120
  action,
847
1121
  requestId: value
@@ -872,7 +1146,7 @@ function parseProvisionBody(body) {
872
1146
  request: requestOf$1(wanted, value.trim())
873
1147
  };
874
1148
  const registry = typeof raw.registry === "string" && raw.registry.trim() !== "" ? raw.registry.trim() : void 0;
875
- const approvedBuilds = stringsOf$2(raw.approvedBuilds);
1149
+ const approvedBuilds = stringsOf$3(raw.approvedBuilds);
876
1150
  const requestId = typeof raw.requestId === "string" && raw.requestId.trim() !== "" ? raw.requestId.trim() : void 0;
877
1151
  return {
878
1152
  ok: true,
@@ -886,6 +1160,7 @@ function parseProvisionBody(body) {
886
1160
  }
887
1161
  //#endregion
888
1162
  //#region ../shared/dist/host/opencli.js
1163
+ var FORBIDDEN_CODES = /* @__PURE__ */ new Set(["FORBIDDEN", "PERMISSION_DENIED"]);
889
1164
  var SITE_SESSION_ARGS = ["--site-session", "persistent"];
890
1165
  var WorkbenchDataError = class extends Error {
891
1166
  code;
@@ -913,6 +1188,102 @@ function opencliCommandLine(command, argv, session = "persistent") {
913
1188
  "json"
914
1189
  ].join(" ");
915
1190
  }
1191
+ function createOpencli(options) {
1192
+ const { shell, plugin, logger, command = "opencli", timeoutMs = OPENCLI_TIMEOUT_MS, maxConcurrent = 4 } = options;
1193
+ const inflight = /* @__PURE__ */ new Map();
1194
+ const MAX_CONCURRENT = maxConcurrent;
1195
+ let running = 0;
1196
+ const waiting = [];
1197
+ async function acquire() {
1198
+ if (running < MAX_CONCURRENT) {
1199
+ running += 1;
1200
+ return;
1201
+ }
1202
+ await new Promise((resolve) => waiting.push(resolve));
1203
+ }
1204
+ function release() {
1205
+ const next = waiting.shift();
1206
+ if (next === void 0) running -= 1;
1207
+ else next();
1208
+ }
1209
+ async function runLimited(task) {
1210
+ await acquire();
1211
+ try {
1212
+ return await task();
1213
+ } finally {
1214
+ release();
1215
+ }
1216
+ }
1217
+ async function call(commandLine, workdir) {
1218
+ let result;
1219
+ try {
1220
+ result = await runShell(shell, commandLine, {
1221
+ ...workdir === void 0 ? {} : { workdir },
1222
+ timeoutMs
1223
+ });
1224
+ } catch (error) {
1225
+ throw new WorkbenchDataError("cli-exec-failed", `执行 opencli 失败:${String(error)}`, { hint: "确认 opencli 已安装且在 PATH 里(which opencli)。" });
1226
+ }
1227
+ if (result.timedOut) throw new WorkbenchDataError("cli-timeout", `opencli 超过 ${Math.round(timeoutMs / 1e3)} 秒没有返回`, { hint: "无头浏览器可能没起来,重试一次通常就好。" });
1228
+ if (result.exitCode !== 0) {
1229
+ const detail = tailLines(result.stderr || result.stdout, 6);
1230
+ const envelope = parseCliErrorEnvelope(result.stderr) ?? parseCliErrorEnvelope(result.stdout);
1231
+ const url = envelope === void 0 ? void 0 : helpUrlOf(envelope);
1232
+ if (envelope?.code === "AUTH_REQUIRED") throw new WorkbenchDataError("auth-required", envelope.message, {
1233
+ detail,
1234
+ ...url === void 0 ? {} : { helpUrl: url }
1235
+ });
1236
+ if (envelope !== void 0 && FORBIDDEN_CODES.has(envelope.code)) throw new WorkbenchDataError("permission-denied", envelope.message, {
1237
+ detail,
1238
+ ...url === void 0 ? {} : { helpUrl: url }
1239
+ });
1240
+ if (envelope !== void 0) throw new WorkbenchDataError("cli-command-failed", `${envelope.code}:${envelope.message}`, {
1241
+ detail,
1242
+ ...envelope.help === void 0 ? {} : { hint: envelope.help }
1243
+ });
1244
+ throw new WorkbenchDataError("cli-unclassified", `opencli 这条命令没跑通(退出码 ${result.exitCode}),而且没有结构化错误码`, {
1245
+ detail,
1246
+ hint: "多半是插件里没有这条命令(版本旧就会缺),也可能是命令自己失败了:先确认命令存在,再更新插件重试。"
1247
+ });
1248
+ }
1249
+ const parsed = parseJsonLoose(result.stdout);
1250
+ if (parsed === void 0) throw new WorkbenchDataError("cli-bad-output", "opencli 输出不是 JSON", { detail: tailLines(result.stdout, 4) });
1251
+ return parsed;
1252
+ }
1253
+ async function json(args, callOptions = {}) {
1254
+ const { key, workdir } = callOptions;
1255
+ const session = callOptions.session ?? "persistent";
1256
+ const callKey = key ?? args.map((arg) => String(arg)).join(" ");
1257
+ const pending = inflight.get(callKey);
1258
+ if (pending !== void 0) return pending;
1259
+ const commandLine = opencliCommandLine(command, [plugin, ...args], session);
1260
+ const once = async () => {
1261
+ return {
1262
+ value: await call(commandLine, workdir),
1263
+ at: Date.now()
1264
+ };
1265
+ };
1266
+ const task = runLimited(async () => {
1267
+ try {
1268
+ return await once();
1269
+ } catch (error) {
1270
+ if (!(error instanceof WorkbenchDataError) || error.code !== "auth-required") throw error;
1271
+ return await once();
1272
+ }
1273
+ });
1274
+ inflight.set(callKey, task);
1275
+ try {
1276
+ return await task;
1277
+ } catch (error) {
1278
+ const message = error instanceof Error ? error.message : String(error);
1279
+ logger?.warn?.(`[dsh-workbench] opencli 调用失败:${message}`);
1280
+ throw error;
1281
+ } finally {
1282
+ inflight.delete(callKey);
1283
+ }
1284
+ }
1285
+ return { json };
1286
+ }
916
1287
  //#endregion
917
1288
  //#region ../shared/dist/host/write-result.js
918
1289
  function writeResultForClient(value, code) {
@@ -1235,6 +1606,42 @@ function registerActions(ctx, resources, wrote) {
1235
1606
  ...wrote === void 0 ? {} : { wrote }
1236
1607
  });
1237
1608
  }
1609
+ function registerPanelState(ctx, store) {
1610
+ return registerPost(ctx, {
1611
+ prefix: PANEL_STATE_PREFIX,
1612
+ what: "面板运行态",
1613
+ refusedCode: "report-refused",
1614
+ resources: [{
1615
+ resource: "cards",
1616
+ handle(payload) {
1617
+ const parsed = parseCardsReport(payload);
1618
+ if (!parsed.ok) return {
1619
+ ok: false,
1620
+ text: parsed.message
1621
+ };
1622
+ store.cards(parsed.value);
1623
+ return {
1624
+ ok: true,
1625
+ at: Date.now()
1626
+ };
1627
+ }
1628
+ }, {
1629
+ resource: "crash",
1630
+ handle(payload) {
1631
+ const parsed = parseCrashReport(payload);
1632
+ if (!parsed.ok) return {
1633
+ ok: false,
1634
+ text: parsed.message
1635
+ };
1636
+ store.crash(parsed.value);
1637
+ return {
1638
+ ok: true,
1639
+ at: Date.now()
1640
+ };
1641
+ }
1642
+ }]
1643
+ });
1644
+ }
1238
1645
  //#endregion
1239
1646
  //#region ../shared/dist/host/long-task.js
1240
1647
  var LIVE = /* @__PURE__ */ new Set(["running", "needs-login"]);
@@ -1447,7 +1854,7 @@ var PLUGIN_LEDGER_FILE = "plugins.json";
1447
1854
  function ledgerKeyOf(entry) {
1448
1855
  return `${entry.opencliHome}\u0000${entry.package}`;
1449
1856
  }
1450
- function textOf$2(value) {
1857
+ function textOf$3(value) {
1451
1858
  if (typeof value !== "string") return void 0;
1452
1859
  const trimmed = value.trim();
1453
1860
  return trimmed === "" ? void 0 : trimmed;
@@ -1455,13 +1862,13 @@ function textOf$2(value) {
1455
1862
  function entryOf(value) {
1456
1863
  if (typeof value !== "object" || value === null) return void 0;
1457
1864
  const box = value;
1458
- const opencliHome = textOf$2(box["opencliHome"]);
1459
- const pkg = textOf$2(box["package"]);
1460
- const installedAt = textOf$2(box["installedAt"]);
1865
+ const opencliHome = textOf$3(box["opencliHome"]);
1866
+ const pkg = textOf$3(box["package"]);
1867
+ const installedAt = textOf$3(box["installedAt"]);
1461
1868
  if (opencliHome === void 0 || pkg === void 0 || installedAt === void 0) return void 0;
1462
- const name = textOf$2(box["name"]);
1463
- const registry = textOf$2(box["registry"]);
1464
- const releasedAt = textOf$2(box["releasedAt"]);
1869
+ const name = textOf$3(box["name"]);
1870
+ const registry = textOf$3(box["registry"]);
1871
+ const releasedAt = textOf$3(box["releasedAt"]);
1465
1872
  return {
1466
1873
  opencliHome,
1467
1874
  package: pkg,
@@ -1628,15 +2035,48 @@ function resolvePluginSource(source, env, options = {}) {
1628
2035
  }
1629
2036
  //#endregion
1630
2037
  //#region ../shared/dist/host/tool-approval.js
1631
- function messageOf(error) {
2038
+ var APPROVAL_NOTE_ARG = "approval_note";
2039
+ var NOTE_SCHEMA = {
2040
+ type: "string",
2041
+ description: "一句话说明你这次想拿它干什么(要点头的那几通会显示在审批屏上)。"
2042
+ };
2043
+ function withApprovalNote(parameters, options = {}) {
2044
+ const properties = parameters["properties"];
2045
+ if (typeof properties !== "object" || properties === null) return parameters;
2046
+ const map = properties;
2047
+ if ("approval_note" in map) return parameters;
2048
+ const kept = (Array.isArray(parameters["required"]) ? parameters["required"] : []).filter((name) => name !== APPROVAL_NOTE_ARG);
2049
+ const required = options.required === true ? [...kept, APPROVAL_NOTE_ARG] : kept;
2050
+ return {
2051
+ ...parameters,
2052
+ properties: {
2053
+ ...map,
2054
+ [APPROVAL_NOTE_ARG]: NOTE_SCHEMA
2055
+ },
2056
+ ...required.length === 0 ? {} : { required }
2057
+ };
2058
+ }
2059
+ function approvalNoteOf(call) {
2060
+ const raw = call.arguments;
2061
+ const value = typeof raw === "object" && raw !== null ? raw[APPROVAL_NOTE_ARG] : void 0;
2062
+ if (typeof value !== "string") return void 0;
2063
+ const line = value.replace(/\s+/g, " ").trim();
2064
+ if (line === "") return void 0;
2065
+ return line.length > 160 ? `${line.slice(0, 160)}…` : line;
2066
+ }
2067
+ function messageOf$1(error) {
1632
2068
  return error instanceof Error ? error.message : String(error);
1633
2069
  }
1634
- function compose(detail, verdict) {
1635
- if (detail === void 0 || typeof detail.zh !== "string" || detail.zh.trim() === "") return verdict.fallback;
1636
- return {
2070
+ function compose(detail, verdict, note) {
2071
+ const base = detail === void 0 || typeof detail.zh !== "string" || detail.zh.trim() === "" ? verdict.fallback : {
1637
2072
  en: `${detail.en} — ${verdict.question.en}`,
1638
2073
  zh: `${detail.zh} —— ${verdict.question.zh}`
1639
2074
  };
2075
+ if (note === void 0) return base;
2076
+ return {
2077
+ en: `${base.en} — ${note}`,
2078
+ zh: `${base.zh} —— ${note}`
2079
+ };
1640
2080
  }
1641
2081
  async function approvalFor(call, context = {}) {
1642
2082
  if (context.writes !== true) return void 0;
@@ -1646,7 +2086,7 @@ async function approvalFor(call, context = {}) {
1646
2086
  } catch (error) {
1647
2087
  return {
1648
2088
  kind: "deny",
1649
- reason: `问"要不要审批"的时候套件的策略抛了错,已经停下:${messageOf(error)}`
2089
+ reason: `问"要不要审批"的时候套件的策略抛了错,已经停下:${messageOf$1(error)}`
1650
2090
  };
1651
2091
  }
1652
2092
  if (verdict?.kind === "read") return void 0;
@@ -1660,7 +2100,7 @@ async function approvalFor(call, context = {}) {
1660
2100
  } catch (error) {
1661
2101
  return {
1662
2102
  kind: "deny",
1663
- reason: `拿不到"这次会发生什么",已经停下:${messageOf(error)}`
2103
+ reason: `拿不到"这次会发生什么",已经停下:${messageOf$1(error)}`
1664
2104
  };
1665
2105
  }
1666
2106
  if (facts !== void 0 && "blocked" in facts) return {
@@ -1670,7 +2110,7 @@ async function approvalFor(call, context = {}) {
1670
2110
  return {
1671
2111
  kind: "ask",
1672
2112
  reason: verdict.reason,
1673
- displayReason: compose(facts, verdict)
2113
+ displayReason: compose(facts, verdict, approvalNoteOf(call))
1674
2114
  };
1675
2115
  }
1676
2116
  //#endregion
@@ -1679,6 +2119,15 @@ function sessionWorkspace(exec) {
1679
2119
  const cwd = exec?.agent?.session?.header?.cwd;
1680
2120
  return typeof cwd === "string" && cwd.trim() !== "" ? cwd.trim() : null;
1681
2121
  }
2122
+ function textOutput(text) {
2123
+ return [{
2124
+ type: "text",
2125
+ text
2126
+ }];
2127
+ }
2128
+ function jsonText(value) {
2129
+ return JSON.stringify(value);
2130
+ }
1682
2131
  //#endregion
1683
2132
  //#region ../shared/dist/host/value-handoff.js
1684
2133
  function handoffId(entry) {
@@ -2000,7 +2449,7 @@ function profileDirOf(ctx) {
2000
2449
  const dir = typeof profile?.dir === "string" ? profile.dir.trim() : "";
2001
2450
  return dir === "" ? null : dir;
2002
2451
  }
2003
- function stringsOf$1(value) {
2452
+ function stringsOf$2(value) {
2004
2453
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
2005
2454
  }
2006
2455
  function profileManifestOf(profileDir) {
@@ -2014,7 +2463,7 @@ function profileManifestOf(profileDir) {
2014
2463
  function profileModules(profileDir) {
2015
2464
  const record = profileManifestOf(profileDir);
2016
2465
  if (record === void 0) return [];
2017
- const bundles = stringsOf$1(asRecord$5(asRecord$5(record.dsh)?.profile)?.bundles);
2466
+ const bundles = stringsOf$2(asRecord$5(asRecord$5(record.dsh)?.profile)?.bundles);
2018
2467
  const dependencies = asRecord$5(record.dependencies) ?? {};
2019
2468
  const names = bundles.length > 0 ? bundles : Object.keys(dependencies);
2020
2469
  const found = [];
@@ -2025,11 +2474,27 @@ function profileModules(profileDir) {
2025
2474
  unit: read.unit,
2026
2475
  version: read.version,
2027
2476
  ...read.registry === void 0 ? {} : { registry: read.registry },
2028
- local: asText$3(dependencies[read.pkg]).startsWith("link:")
2477
+ local: isLocalDependencySpec(asText$3(dependencies[read.pkg]))
2029
2478
  });
2030
2479
  }
2031
2480
  return found;
2032
2481
  }
2482
+ function createDeclarationLookup(profileDir) {
2483
+ let roster;
2484
+ const scan = () => new Map(profileModules(profileDir).map((row) => [row.unit, row.pkg]));
2485
+ return (unit) => {
2486
+ roster ??= scan();
2487
+ let pkg = roster.get(unit);
2488
+ if (pkg === void 0) {
2489
+ roster = scan();
2490
+ pkg = roster.get(unit);
2491
+ }
2492
+ return pkg === void 0 ? void 0 : {
2493
+ pkg,
2494
+ declaration: moduleDeclarationOf(profileDir, pkg)
2495
+ };
2496
+ };
2497
+ }
2033
2498
  function registryFromLockfile(profileDir, pkg) {
2034
2499
  let text;
2035
2500
  try {
@@ -2070,6 +2535,20 @@ function declarationGaps(profileDir) {
2070
2535
  }
2071
2536
  return gaps;
2072
2537
  }
2538
+ function declaredPluginCommands(profileDir) {
2539
+ const declared = /* @__PURE__ */ new Map();
2540
+ for (const module of profileModules(profileDir)) {
2541
+ const read = readDeclaration(module.pkg, moduleDeclarationOf(profileDir, module.pkg));
2542
+ for (const need of read.manifest?.env?.plugins ?? []) {
2543
+ const name = need.name.trim();
2544
+ if (name === "") continue;
2545
+ const commands = declared.get(name) ?? /* @__PURE__ */ new Set();
2546
+ for (const command of need.commands ?? []) commands.add(command);
2547
+ declared.set(name, commands);
2548
+ }
2549
+ }
2550
+ return declared;
2551
+ }
2073
2552
  function readInstalledModule(profileDir, pkg) {
2074
2553
  if (profileDir === null || !isPackageName(pkg)) return {
2075
2554
  kind: "unknown",
@@ -2105,6 +2584,32 @@ function readInstalledModule(profileDir, pkg) {
2105
2584
  ...registry === "" ? {} : { registry }
2106
2585
  };
2107
2586
  }
2587
+ function isLocalDependencySpec(spec) {
2588
+ const trimmed = spec.trim();
2589
+ return trimmed.startsWith("link:") || trimmed.startsWith("file:") || trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("/");
2590
+ }
2591
+ function readProfilePackage(profileDir, pkg) {
2592
+ if (profileDir === null || !isPackageName(pkg)) return void 0;
2593
+ let manifest;
2594
+ try {
2595
+ manifest = JSON.parse(fs.readFileSync(path.join(profileDir, "node_modules", pkg, "package.json"), "utf8"));
2596
+ } catch {
2597
+ return;
2598
+ }
2599
+ const record = asRecord$5(manifest);
2600
+ if (record === void 0) return void 0;
2601
+ const version = asText$3(record.version);
2602
+ if (version === "") return void 0;
2603
+ const declaredRegistry = asText$3(asRecord$5(record.publishConfig)?.registry);
2604
+ const registry = declaredRegistry === "" ? registryFromLockfile(profileDir, pkg) : declaredRegistry;
2605
+ const local = isLocalDependencySpec(asText$3((asRecord$5(profileManifestOf(profileDir)?.dependencies) ?? {})[pkg]));
2606
+ return {
2607
+ pkg,
2608
+ version,
2609
+ ...registry === "" ? {} : { registry },
2610
+ ...local ? { local: true } : {}
2611
+ };
2612
+ }
2108
2613
  //#endregion
2109
2614
  //#region src/shared/api/host/profile-registries.ts
2110
2615
  var REGISTRY_MARKER = "# dsh-workbench 写的:这一行的包只在这个源上(dsh 卸载没有源参数,pnpm 得问得到)";
@@ -2350,6 +2855,13 @@ var panelsApproval = (call) => {
2350
2855
  zh: `agent 想${verb} opencli 插件 ${what}(会改这台机器上装的东西)。允许吗?`
2351
2856
  });
2352
2857
  }
2858
+ if (action === "update-workbench") return askOf("workbench_modules update-workbench replaces the workbench itself with the new version from the registry", {
2859
+ en: "it updates the workbench itself on this machine (the panel picks the new version up by itself). Allow?",
2860
+ zh: "会把工作台自己更新到源上的新版(面板会自己换新)。允许吗?"
2861
+ }, {
2862
+ en: "The agent wants to update the workbench itself to the new version from the registry (it changes what is installed on this machine; the panel picks the new version up by itself). Allow?",
2863
+ zh: "agent 想把工作台自己更新到源上的新版(会改这台机器上装的东西,面板会自己换新)。允许吗?"
2864
+ });
2353
2865
  if (action === "update-dependency") return askOf(`workbench_modules update-dependency ${id ?? ""} upgrades a shared dependency on this machine`, {
2354
2866
  en: "it upgrades that shared dependency on this machine. Allow?",
2355
2867
  zh: `会升级共用依赖 ${what}。允许吗?`
@@ -2423,46 +2935,99 @@ var EMPTY_BOARD = {
2423
2935
  };
2424
2936
  var BOARD_TARGETS = ["workspace", "machine"];
2425
2937
  var DEFAULT_BOARD_TARGET = "workspace";
2426
- function resolveScope(query) {
2427
- const { id, boards } = query;
2428
- const board = boards.workspace ?? boards.machine;
2938
+ function overrideOf(board, id) {
2939
+ if (board === null) return void 0;
2940
+ if (board.hidden.includes(id)) return false;
2941
+ if (board.shown.includes(id)) return true;
2942
+ }
2943
+ function defaultOf(board, id) {
2429
2944
  if (board === null) return false;
2430
2945
  if (board.hidden.includes(id)) return false;
2431
2946
  return board.shown.includes(id);
2432
2947
  }
2948
+ function resolveScope(query) {
2949
+ const { id, boards } = query;
2950
+ return overrideOf(boards.workspace, id) ?? defaultOf(boards.machine, id);
2951
+ }
2433
2952
  function strings(raw) {
2434
2953
  return Array.isArray(raw) ? raw.filter((item) => typeof item === "string") : [];
2435
2954
  }
2436
2955
  function parseBoard(raw) {
2437
2956
  if (typeof raw !== "object" || raw === null) return EMPTY_BOARD;
2438
2957
  const value = raw;
2958
+ const order = strings(value.order);
2439
2959
  return {
2440
2960
  shown: strings(value.shown),
2441
- hidden: strings(value.hidden)
2961
+ hidden: strings(value.hidden),
2962
+ ...order.length === 0 ? {} : { order }
2442
2963
  };
2443
2964
  }
2444
2965
  function parseReleased(raw) {
2445
2966
  if (typeof raw !== "object" || raw === null) return [];
2446
2967
  return strings(raw.ids);
2447
2968
  }
2448
- function resolvePanels(manifests, boards) {
2449
- const ordered = boards.workspace?.shown ?? boards.machine?.shown ?? [];
2450
- const rank = new Map(ordered.map((id, index) => [id, index]));
2451
- return manifests.filter((manifest) => manifest.card !== null).map((manifest) => {
2969
+ function resolvePanels(manifests, boards, owned = /* @__PURE__ */ new Set()) {
2970
+ const listed = manifests.filter((manifest) => manifest.card !== null || !owned.has(manifest.id));
2971
+ const carded = listed.filter((manifest) => manifest.card !== null);
2972
+ const declared = (manifest) => manifest.card?.order ?? 0;
2973
+ const byDeclared = (a, b) => declared(a) - declared(b) || a.id.localeCompare(b.id);
2974
+ const queued = [...new Set(boards.workspace?.order ?? [])].map((id) => carded.find((manifest) => manifest.id === id)).filter((manifest) => manifest !== void 0).filter((manifest) => resolveScope({
2975
+ id: manifest.id,
2976
+ boards
2977
+ }));
2978
+ const placed = new Set(queued.map((manifest) => manifest.id));
2979
+ const ordered = [...queued, ...carded.filter((manifest) => !placed.has(manifest.id) && resolveScope({
2980
+ id: manifest.id,
2981
+ boards
2982
+ })).sort(byDeclared)];
2983
+ const rank = new Map(ordered.map((manifest, index) => [manifest.id, index]));
2984
+ return listed.map((manifest) => {
2985
+ const hasCard = manifest.card !== null;
2452
2986
  const on = resolveScope({
2453
2987
  id: manifest.id,
2454
2988
  boards
2455
2989
  });
2456
- const order = rank.get(manifest.id) ?? ordered.length + (manifest.card?.order ?? 0);
2990
+ const order = hasCard ? rank.get(manifest.id) ?? ordered.length + declared(manifest) : 0;
2457
2991
  return {
2458
2992
  id: manifest.id,
2459
2993
  titleKey: manifest.card?.titleKey ?? manifest.id,
2460
2994
  order,
2461
2995
  shown: on,
2462
- source: on && (boards.workspace?.shown.includes(manifest.id) ?? false) ? "user" : "default",
2463
- hasCard: true
2996
+ source: overrideOf(boards.workspace, manifest.id) === void 0 ? "default" : "user",
2997
+ hasCard
2998
+ };
2999
+ }).sort((a, b) => {
3000
+ if (a.hasCard !== b.hasCard) return a.hasCard ? -1 : 1;
3001
+ return a.order - b.order || a.id.localeCompare(b.id);
3002
+ });
3003
+ }
3004
+ var PLUGIN_PANEL_PREFIX = "plugin:";
3005
+ function pluginPanelId(pkg) {
3006
+ return `${PLUGIN_PANEL_PREFIX}${pkg}`;
3007
+ }
3008
+ function resolvePluginPanels(plugins, boards) {
3009
+ const ordered = [...new Set(boards.workspace?.order ?? [])];
3010
+ const rank = new Map(ordered.map((id, index) => [id, index]));
3011
+ const seen = /* @__PURE__ */ new Set();
3012
+ return plugins.map((plugin) => {
3013
+ const id = pluginPanelId(plugin.pkg);
3014
+ return {
3015
+ id,
3016
+ pkg: plugin.pkg,
3017
+ title: plugin.title,
3018
+ ...plugin.name === void 0 ? {} : { name: plugin.name },
3019
+ order: rank.get(id) ?? ordered.length,
3020
+ shown: resolveScope({
3021
+ id,
3022
+ boards
3023
+ }),
3024
+ source: overrideOf(boards.workspace, id) === void 0 ? "default" : "user"
2464
3025
  };
2465
- }).sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
3026
+ }).filter((entry) => {
3027
+ if (seen.has(entry.id)) return false;
3028
+ seen.add(entry.id);
3029
+ return true;
3030
+ }).sort((a, b) => a.order - b.order || a.pkg.localeCompare(b.pkg));
2466
3031
  }
2467
3032
  function withoutReleased(released, id) {
2468
3033
  return released.filter((item) => item !== id);
@@ -2476,16 +3041,22 @@ function added(list, id) {
2476
3041
  function dropped(list, id) {
2477
3042
  return list.filter((item) => item !== id);
2478
3043
  }
2479
- function withShown(board, id) {
2480
- return {
3044
+ function withShown(board, id, visible) {
3045
+ const next = {
2481
3046
  shown: added(board.shown, id),
2482
3047
  hidden: dropped(board.hidden, id)
2483
3048
  };
3049
+ return visible === void 0 ? next : {
3050
+ ...next,
3051
+ order: added(board.order ?? visible, id)
3052
+ };
2484
3053
  }
2485
3054
  function withHidden(board, id) {
3055
+ const order = dropped(board.order ?? [], id);
2486
3056
  return {
2487
3057
  shown: dropped(board.shown, id),
2488
- hidden: added(board.hidden, id)
3058
+ hidden: added(board.hidden, id),
3059
+ ...order.length === 0 ? {} : { order }
2489
3060
  };
2490
3061
  }
2491
3062
  function withoutPanel(board, id) {
@@ -2494,120 +3065,64 @@ function withoutPanel(board, id) {
2494
3065
  hidden: dropped(board.hidden, id)
2495
3066
  };
2496
3067
  }
2497
- function withMoved(board, id, index) {
2498
- const current = [...board.shown];
3068
+ function withMoved(board, id, index, visible) {
3069
+ const current = visible.includes(id) ? [...visible] : [...visible, id];
2499
3070
  const from = current.indexOf(id);
2500
- const max = from === -1 ? current.length : current.length - 1;
2501
- const to = Math.max(0, Math.min(max, Math.trunc(index)));
2502
- if (from === -1) current.splice(to, 0, id);
2503
- else if (from !== to) current.splice(to, 0, ...current.splice(from, 1));
3071
+ const to = Math.max(0, Math.min(current.length - 1, Math.trunc(index)));
3072
+ if (from !== to) current.splice(to, 0, ...current.splice(from, 1));
2504
3073
  return {
2505
- shown: current,
2506
- hidden: dropped(board.hidden, id)
3074
+ shown: added(board.shown, id),
3075
+ hidden: dropped(board.hidden, id),
3076
+ order: current
2507
3077
  };
2508
3078
  }
2509
3079
  //#endregion
2510
- //#region src/shared/api/host/diy-facts.ts
2511
- function asExported(dir) {
3080
+ //#region src/shared/api/host/opencli-command.ts
3081
+ var OPENCLI_PATH_FALLBACK = "opencli";
3082
+ function isolatedOpencliPathOf(ctx) {
3083
+ const homePath = ctx.get("dshHomePath");
3084
+ if (typeof homePath !== "function") return null;
2512
3085
  try {
2513
- const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
2514
- return (typeof manifest.exports === "object" && manifest.exports !== null ? Object.values(manifest.exports) : []).filter((one) => typeof one === "string");
3086
+ return join(homePath("workbench/opencli/bin/opencli"));
2515
3087
  } catch {
2516
- return [];
3088
+ return null;
2517
3089
  }
2518
3090
  }
2519
- function mtimeOf(file) {
3091
+ function opencliCommandOf(ctx) {
3092
+ const isolated = isolatedOpencliPathOf(ctx);
3093
+ const fromEnv = process.env.DSH_WORKBENCH_OPENCLI;
3094
+ if (fromEnv !== void 0 && fromEnv !== "") return {
3095
+ command: fromEnv,
3096
+ source: "env",
3097
+ isolated
3098
+ };
3099
+ if (isolated !== null && existsSync(isolated)) return {
3100
+ command: isolated,
3101
+ source: "isolated",
3102
+ isolated
3103
+ };
3104
+ return {
3105
+ command: OPENCLI_PATH_FALLBACK,
3106
+ source: "path",
3107
+ isolated
3108
+ };
3109
+ }
3110
+ //#endregion
3111
+ //#region src/shared/api/host/diy-recon.ts
3112
+ var SITE_LIMIT = 6;
3113
+ var COMMAND_LIMIT = 12;
3114
+ var SHOWN_LIMIT = 6;
3115
+ var PLUGIN_PREFIX = "opencli-plugin-";
3116
+ function text(value) {
3117
+ return typeof value === "string" ? value.trim() : "";
3118
+ }
3119
+ function stringsOf$1(value) {
3120
+ return Array.isArray(value) ? value.filter((one) => typeof one === "string") : [];
3121
+ }
3122
+ function manifestCommandsOf(raw) {
3123
+ let parsed;
2520
3124
  try {
2521
- return statSync(file).mtimeMs;
2522
- } catch {
2523
- return;
2524
- }
2525
- }
2526
- function newestMtime(dir) {
2527
- let newest;
2528
- const walk = (at) => {
2529
- let entries;
2530
- try {
2531
- entries = readdirSync(at, { withFileTypes: true });
2532
- } catch {
2533
- return;
2534
- }
2535
- for (const entry of entries) {
2536
- const full = join(at, entry.name);
2537
- if (entry.isDirectory()) walk(full);
2538
- else if (entry.isFile()) {
2539
- const stamp = mtimeOf(full);
2540
- if (stamp !== void 0 && (newest === void 0 || stamp > newest)) newest = stamp;
2541
- }
2542
- }
2543
- };
2544
- walk(dir);
2545
- return newest;
2546
- }
2547
- function moduleBuildFacts(dir) {
2548
- const artifacts = asExported(dir).map((rel) => ({
2549
- path: rel,
2550
- mtime: mtimeOf(join(dir, rel))
2551
- }));
2552
- const present = artifacts.filter((one) => one.mtime !== void 0);
2553
- const oldestBuilt = present.reduce((min, one) => min === void 0 || one.mtime < min ? one.mtime : min, void 0);
2554
- const newestSource = newestMtime(join(dir, "src"));
2555
- return {
2556
- artifacts: artifacts.map((one) => ({
2557
- path: one.path,
2558
- at: one.mtime === void 0 ? null : new Date(one.mtime).toISOString()
2559
- })),
2560
- builtAll: present.length === artifacts.length && artifacts.length > 0,
2561
- stale: newestSource !== void 0 && oldestBuilt !== void 0 && newestSource > oldestBuilt
2562
- };
2563
- }
2564
- //#endregion
2565
- //#region src/shared/api/host/opencli-command.ts
2566
- var OPENCLI_PATH_FALLBACK = "opencli";
2567
- function isolatedOpencliPathOf(ctx) {
2568
- const homePath = ctx.get("dshHomePath");
2569
- if (typeof homePath !== "function") return null;
2570
- try {
2571
- return join(homePath("workbench/opencli/bin/opencli"));
2572
- } catch {
2573
- return null;
2574
- }
2575
- }
2576
- function opencliCommandOf(ctx) {
2577
- const isolated = isolatedOpencliPathOf(ctx);
2578
- const fromEnv = process.env.DSH_WORKBENCH_OPENCLI;
2579
- if (fromEnv !== void 0 && fromEnv !== "") return {
2580
- command: fromEnv,
2581
- source: "env",
2582
- isolated
2583
- };
2584
- if (isolated !== null && existsSync(isolated)) return {
2585
- command: isolated,
2586
- source: "isolated",
2587
- isolated
2588
- };
2589
- return {
2590
- command: OPENCLI_PATH_FALLBACK,
2591
- source: "path",
2592
- isolated
2593
- };
2594
- }
2595
- //#endregion
2596
- //#region src/shared/api/host/diy-recon.ts
2597
- var SITE_LIMIT = 6;
2598
- var COMMAND_LIMIT = 12;
2599
- var SHOWN_LIMIT = 6;
2600
- var PLUGIN_PREFIX = "opencli-plugin-";
2601
- function text(value) {
2602
- return typeof value === "string" ? value.trim() : "";
2603
- }
2604
- function stringsOf(value) {
2605
- return Array.isArray(value) ? value.filter((one) => typeof one === "string") : [];
2606
- }
2607
- function manifestCommandsOf(raw) {
2608
- let parsed;
2609
- try {
2610
- parsed = JSON.parse(raw);
3125
+ parsed = JSON.parse(raw);
2611
3126
  } catch {
2612
3127
  return [];
2613
3128
  }
@@ -2632,7 +3147,7 @@ function manifestCommandsOf(raw) {
2632
3147
  strategy: text(row["strategy"]).toLowerCase(),
2633
3148
  browser: row["browser"] === true,
2634
3149
  example: text(row["example"]),
2635
- columns: stringsOf(row["columns"])
3150
+ columns: stringsOf$1(row["columns"])
2636
3151
  });
2637
3152
  }
2638
3153
  return commands;
@@ -2724,7 +3239,7 @@ function reconOf(commands, plugins, keywords) {
2724
3239
  };
2725
3240
  return {
2726
3241
  ...withNext,
2727
- text: textOf$1(withNext)
3242
+ text: textOf$2(withNext)
2728
3243
  };
2729
3244
  }
2730
3245
  function nextOf(answer) {
@@ -2737,7 +3252,7 @@ function nextOf(answer) {
2737
3252
  if (plugin !== void 0) return `这台机器另装的插件 ${plugin.name} 里有匹配:opencli ${plugin.name} --help 看全部命令`;
2738
3253
  return "这台机器上没有现成的 —— 两条路:① 给这个站点写 adapter:opencli skills read opencli-adapter-author;② 只读一次:workbench_diy action=probe(script 读接口 / css 读 DOM)";
2739
3254
  }
2740
- function textOf$1(answer) {
3255
+ function textOf$2(answer) {
2741
3256
  const lines = [];
2742
3257
  if (answer.sites.length > 0) {
2743
3258
  lines.push(`opencli 自带这 ${String(answer.sites.length)} 组能力(不用装、不用点头):`);
@@ -2781,10 +3296,169 @@ function installedPluginsOf(dir = vendorPluginsDir()) {
2781
3296
  }));
2782
3297
  }
2783
3298
  //#endregion
3299
+ //#region src/shared/api/host/diy-facts.ts
3300
+ function asExported(dir) {
3301
+ try {
3302
+ const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
3303
+ return (typeof manifest.exports === "object" && manifest.exports !== null ? Object.values(manifest.exports) : []).filter((one) => typeof one === "string");
3304
+ } catch {
3305
+ return [];
3306
+ }
3307
+ }
3308
+ function mtimeOf(file) {
3309
+ try {
3310
+ return statSync(file).mtimeMs;
3311
+ } catch {
3312
+ return;
3313
+ }
3314
+ }
3315
+ function newestMtime(dir) {
3316
+ let newest;
3317
+ const walk = (at) => {
3318
+ let entries;
3319
+ try {
3320
+ entries = readdirSync(at, { withFileTypes: true });
3321
+ } catch {
3322
+ return;
3323
+ }
3324
+ for (const entry of entries) {
3325
+ const full = join(at, entry.name);
3326
+ if (entry.isDirectory()) walk(full);
3327
+ else if (entry.isFile()) {
3328
+ const stamp = mtimeOf(full);
3329
+ if (stamp !== void 0 && (newest === void 0 || stamp > newest)) newest = stamp;
3330
+ }
3331
+ }
3332
+ };
3333
+ walk(dir);
3334
+ return newest;
3335
+ }
3336
+ function moduleBuildFacts(dir) {
3337
+ const artifacts = asExported(dir).map((rel) => ({
3338
+ path: rel,
3339
+ mtime: mtimeOf(join(dir, rel))
3340
+ }));
3341
+ const present = artifacts.filter((one) => one.mtime !== void 0);
3342
+ const oldestBuilt = present.reduce((min, one) => min === void 0 || one.mtime < min ? one.mtime : min, void 0);
3343
+ const newestSource = newestMtime(join(dir, "src"));
3344
+ return {
3345
+ artifacts: artifacts.map((one) => ({
3346
+ path: one.path,
3347
+ at: one.mtime === void 0 ? null : new Date(one.mtime).toISOString()
3348
+ })),
3349
+ builtAll: present.length === artifacts.length && artifacts.length > 0,
3350
+ stale: newestSource !== void 0 && oldestBuilt !== void 0 && newestSource > oldestBuilt
3351
+ };
3352
+ }
3353
+ //#endregion
3354
+ //#region src/modules/panels/model/diy-report.ts
3355
+ function healthOf(options) {
3356
+ const { registry, unit, installed } = options;
3357
+ const refused = registry?.refusals().find((one) => one.unit === unit);
3358
+ if (refused !== void 0) return {
3359
+ state: "failed",
3360
+ reason: refused.reasons.join(";")
3361
+ };
3362
+ const failed = registry?.failureOf(unit);
3363
+ if (failed !== void 0) return {
3364
+ state: "failed",
3365
+ reason: failed
3366
+ };
3367
+ if (registry?.all().some((one) => one.unit === unit) === true) return { state: "ok" };
3368
+ return {
3369
+ state: "absent",
3370
+ reason: installed ? "工作台还没读到它的声明 —— 新装的模块要重启一次工作台" : "还没装"
3371
+ };
3372
+ }
3373
+ function healthText(health) {
3374
+ if (health.state === "ok") return "是";
3375
+ if (health.state === "failed") return `不是 —— ${health.reason ?? "(没给原因)"}`;
3376
+ return `还没有(${health.reason ?? "还不知道"})`;
3377
+ }
3378
+ function ageText(at, now) {
3379
+ const seconds = Math.max(0, Math.round((now - at) / 1e3));
3380
+ if (seconds < 10) return "刚刚";
3381
+ if (seconds < 60) return `${String(seconds)} 秒前`;
3382
+ const minutes = Math.round(seconds / 60);
3383
+ if (minutes < 60) return `${String(minutes)} 分钟前`;
3384
+ return `${String(Math.round(minutes / 60))} 小时前`;
3385
+ }
3386
+ function stampOf(at) {
3387
+ return `${new Date(at).toISOString().slice(11, 19)}Z`;
3388
+ }
3389
+ function readText$1(read, now) {
3390
+ const when = `${stampOf(read.at)} · ${ageText(read.at, now)}`;
3391
+ if (read.phase === "ready") return `${read.route} 读到了(${when})`;
3392
+ if (read.phase === "loading") return `${read.route} 正在读(${when})`;
3393
+ if (read.phase === "idle") return `${read.route} 没在取数(${when})`;
3394
+ const code = read.error?.code ?? "unknown";
3395
+ const message = read.error?.message ?? "";
3396
+ return `${read.route} 读不到(${code})${message === "" ? "" : `:${message}`}(${when})`;
3397
+ }
3398
+ function renderPanel(options) {
3399
+ const { packagesDir, workspace, rows, board, at, now } = options;
3400
+ const head = `工坊:${packagesDir}\n这个工作区:${workspace ?? "(会话不在任何工作区上)"}`;
3401
+ const note = at === 0 ? "\n还没有一条面板运行态上报 —— 面板上一个取数 / 渲染都没发生过:要么这个工作区的面板上没开卡片,要么用户那一页还是刷新前的旧版本(上报要页面刷一次才带上)。" : `\n面板上有 ${String(board.length)} 张卡|运行态来自用户那一页(最后一次上报 ${stampOf(at)} · ${ageText(at, now)})`;
3402
+ const body = rows.map((row) => {
3403
+ const title = `—— ${row.unit}${row.name === "" ? "" : `(${row.name})`} · ${healthText(row.health)}`;
3404
+ const report = row.report;
3405
+ if (report === void 0) return [title, ` 没上报:卡片没渲染出来,或者用户那一页还没刷新到带这份补丁的版本`].join("\n");
3406
+ const reads = report.cards.slice(0, 6).map((one) => ` 数据 · ${readText$1(one, now)}`);
3407
+ const more = report.cards.length > 6 ? [` 数据 · …还有 ${String(report.cards.length - 6)} 条`] : [];
3408
+ const crash = report.crash === void 0 ? " 崩过:没有" : ` 崩过:是 —— ${report.crash.reason}(${stampOf(report.crash.at)} · 共 ${String(report.crash.count)} 次)`;
3409
+ return [
3410
+ title,
3411
+ ...reads,
3412
+ ...more,
3413
+ crash
3414
+ ].join("\n");
3415
+ });
3416
+ return body.length === 0 ? `${head}${note}` : `${head}${note}\n${body.join("\n")}`;
3417
+ }
3418
+ function statusOf(options) {
3419
+ const { dir, unit, name, installed, known, open, health } = options;
3420
+ const facts = moduleBuildFacts(dir);
3421
+ const next = health.state === "failed" ? `它起不来:${health.reason ?? "(没给原因)"} —— 先把这一条修掉(改模块源码 → build;改了 package.json / cordis.patch.yml 要重启工作台),别去重装` : !installed ? "装:action=install(第一次,会让用户点头一次)" : !known ? "面板还不认得这个单元 —— 重启一次工作台(新装的模块要重启才注册进面板),重启后再 action=open" : !open ? "上板:action=open(只上板,不重装)" : !facts.builtAll ? "还没构建:pnpm exec dsh-wb-module build" : facts.stale ? "源码比产物新:pnpm exec dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新页面)" : "都在生效:改源码 → build(宿主侧秒级热换;改了 client.js 刷新页面),不要再装一遍";
3422
+ return {
3423
+ unit,
3424
+ name,
3425
+ dir,
3426
+ installed,
3427
+ known,
3428
+ open,
3429
+ artifacts: facts.artifacts,
3430
+ stale: facts.stale,
3431
+ health,
3432
+ next
3433
+ };
3434
+ }
3435
+ function renderStatus(packagesDir, lines) {
3436
+ if (lines.length === 0) return renderList(packagesDir, []);
3437
+ return `工坊:${packagesDir}\n${lines.map((one) => {
3438
+ const head = `—— ${one.unit}(${one.name})`;
3439
+ const facts = ` 装:${one.installed ? "是" : "没有"} · 面板认得:${one.known ? "是" : "不是"} · 这个项目上开着:${one.open ? "是" : "没有"}`;
3440
+ const artifacts = one.artifacts.length === 0 ? " 产物:没有" : ` 产物:${one.artifacts.map((item) => `${item.path}${item.at === null ? "(缺)" : ` ${item.at.slice(11, 19)}Z`}`).join(" · ")} · 源码比产物新:${one.stale ? "是" : "没有"}`;
3441
+ return [
3442
+ head,
3443
+ facts,
3444
+ ` 起得来:${healthText(one.health)}`,
3445
+ artifacts,
3446
+ ` 下一步:${one.next}`
3447
+ ].join("\n");
3448
+ }).join("\n")}`;
3449
+ }
3450
+ function renderList(packagesDir, modules) {
3451
+ if (modules.length === 0) return `工坊里还没有模块(${packagesDir})—— 在里面造一个:pnpm exec dsh-wb-module init <名字> --dir <工坊>/packages/module-<名字> --template diy`;
3452
+ const lines = modules.map((one) => `${one.unit} · ${one.installed ? "已装" : "没装"} · ${one.name}` + (one.description === "" ? "" : `\n${" ".repeat(one.unit.length)} ${one.description}`));
3453
+ const footer = modules.filter((one) => !one.installed).length === 0 ? "都已装过 —— **不要再装**:改源码 + dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面)。" : "装:workbench_diy(action=install, unit=<上面那个 unit>)";
3454
+ return `工坊:${packagesDir}\n${lines.join("\n")}\n\n${footer}`;
3455
+ }
3456
+ //#endregion
2784
3457
  //#region src/modules/panels/model/diy-tools.ts
2785
- var ACTIONS$1 = [
3458
+ var ACTIONS$2 = [
2786
3459
  "list",
2787
3460
  "status",
3461
+ "panel",
2788
3462
  "probe",
2789
3463
  "recon",
2790
3464
  "install",
@@ -2840,33 +3514,6 @@ function verdictOf$1(change) {
2840
3514
  const tail = (change.packageResult?.output ?? "").trim().split("\n").slice(-3).join(" | ");
2841
3515
  return `没装上(${change.errorCode ?? "failed"})${tail === "" ? "" : `:${tail}`}`;
2842
3516
  }
2843
- function statusOf(options) {
2844
- const { dir, unit, name, installed, known, open } = options;
2845
- const facts = moduleBuildFacts(dir);
2846
- const next = !installed ? "装:action=install(第一次,会让用户点头一次)" : !known ? "面板还不认得这个单元 —— 重启一次工作台(新装的模块要重启才注册进面板),重启后再 action=open" : !open ? "上板:action=open(只上板,不重装)" : !facts.builtAll ? "还没构建:pnpm exec dsh-wb-module build" : facts.stale ? "源码比产物新:pnpm exec dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新页面)" : "都在生效:改源码 → build(宿主侧秒级热换;改了 client.js 刷新页面),不要再装一遍";
2847
- return {
2848
- unit,
2849
- name,
2850
- dir,
2851
- installed,
2852
- known,
2853
- open,
2854
- artifacts: facts.artifacts,
2855
- stale: facts.stale,
2856
- next
2857
- };
2858
- }
2859
- function renderStatus(packagesDir, lines) {
2860
- if (lines.length === 0) return renderList(packagesDir, []);
2861
- return `工坊:${packagesDir}\n${lines.map((one) => {
2862
- return [
2863
- `—— ${one.unit}(${one.name})`,
2864
- ` 装:${one.installed ? "是" : "没有"} · 面板认得:${one.known ? "是" : "不是"} · 这个项目上开着:${one.open ? "是" : "没有"}`,
2865
- one.artifacts.length === 0 ? " 产物:没有" : ` 产物:${one.artifacts.map((item) => `${item.path}${item.at === null ? "(缺)" : ` ${item.at.slice(11, 19)}Z`}`).join(" · ")} · 源码比产物新:${one.stale ? "是" : "没有"}`,
2866
- ` 下一步:${one.next}`
2867
- ].join("\n");
2868
- }).join("\n")}`;
2869
- }
2870
3517
  function renderProbe(out) {
2871
3518
  if (out.ok) return `读到了 —— 这就是那条读的返回值,把它固化成模块宿主半侧的那一条:\n${(JSON.stringify(out.value, null, 2) ?? "undefined").slice(0, 2e3)}`;
2872
3519
  const lines = [`没读到(${out.code}):${out.message}`];
@@ -2887,12 +3534,6 @@ function renderFind(out) {
2887
3534
  const more = out.entries.length > 20 ? `\n… 还有 ${String(out.entries.length - 20)} 个` : "";
2888
3535
  return `找到了 ${String(out.count)} 个元素 —— 每行的单元格按换行拼在 text 里;把它固化成模块宿主半侧的那一条(host 的 browserFind + 切格 + 按列序取值):\n${head.join("\n")}${more}`;
2889
3536
  }
2890
- function renderList(packagesDir, modules) {
2891
- if (modules.length === 0) return `工坊里还没有模块(${packagesDir})—— 在里面造一个:pnpm exec dsh-wb-module init <名字> --dir <工坊>/packages/module-<名字> --template diy`;
2892
- const lines = modules.map((one) => `${one.unit} · ${one.installed ? "已装" : "没装"} · ${one.name}` + (one.description === "" ? "" : `\n${" ".repeat(one.unit.length)} ${one.description}`));
2893
- const footer = modules.filter((one) => !one.installed).length === 0 ? "都已装过 —— **不要再装**:改源码 + dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面)。" : "装:workbench_diy(action=install, unit=<上面那个 unit>)";
2894
- return `工坊:${packagesDir}\n${lines.join("\n")}\n\n${footer}`;
2895
- }
2896
3537
  function showOrReason(panels, unit, workspace) {
2897
3538
  try {
2898
3539
  panels.show(unit, workspace);
@@ -2912,18 +3553,18 @@ function registerDiyTools(ctx, market, panels) {
2912
3553
  }
2913
3554
  const tool = {
2914
3555
  name: "workbench_diy",
2915
- description: "看 DIY 工坊里的模块现在走到哪一步,或者把它们装上 / 上板。只认工坊里的模块(不是市场 —— 装别的东西用 workbench_modules)。status 会把五件事一次说清(装了没 / 面板认得没 / 这个项目上开着没 / 产物齐不齐 / 源码是否比产物新)并给出下一步,所以别自己去拼这些状态、也别反复 install。recon 是**先看有没有现成的**:拿一个关键词(站点名 / 系统名 / 域名)去 opencli 自带的那份目录里找,回它有哪些命令、要不要用户的 Chrome 登录;都没有就老实说,并给出\"写 adapter\"或\"探一条读\"两条路。用户不知道 opencli 是什么 —— 这一步别问他,自己查。probe 是**探一条读**:用用户浏览器里现有的登录去读一次,把结果与失败码归一(两拍、重试、平台码映射都在里面),探明之后再把它固化成模块宿主半侧的那一条。两种读法二选一:script(页面里跑一段回 JSON 的脚本,读接口)或 css(给一个选择器,读 DOM 那一行 —— 页面里没有接口时用这个,别写 document.querySelectorAll)。装是这台机器上的持久变更,要用户点头;**一辈子只装一次**:之后改源码只需 dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面),不要再调它。",
2916
- parameters: {
3556
+ description: "看 DIY 工坊里的模块现在走到哪一步,或者把它们装上 / 上板。只认工坊里的模块(不是市场 —— 装别的东西用 workbench_modules)。status 会把六件事一次说清(装了没 / 面板认得没 / 这个项目上开着没 / **起得来没** / 产物齐不齐 / 源码是否比产物新)并给出下一步,所以别自己去拼这些状态、也别反复 install。panel 是**面板上现在究竟是什么样**:这个工作区开了哪几张卡、每张最后是读到了还是读不到(带宿主回的原文)、有没有崩过 —— 用户说\"卡片不对 / 一片空白 / 一直转圈\"就先问它,别靠猜、也别让他截图给你。recon 是**先看有没有现成的**:拿一个关键词(站点名 / 系统名 / 域名)去 opencli 自带的那份目录里找,回它有哪些命令、要不要用户的 Chrome 登录;都没有就老实说,并给出\"写 adapter\"或\"探一条读\"两条路。用户不知道 opencli 是什么 —— 这一步别问他,自己查。probe 是**探一条读**:用用户浏览器里现有的登录去读一次,把结果与失败码归一(两拍、重试、平台码映射都在里面),探明之后再把它固化成模块宿主半侧的那一条。两种读法二选一:script(页面里跑一段回 JSON 的脚本,读接口)或 css(给一个选择器,读 DOM 那一行 —— 页面里没有接口时用这个,别写 document.querySelectorAll)。装是这台机器上的持久变更,要用户点头;**一辈子只装一次**:之后改源码只需 dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面),不要再调它。",
3557
+ parameters: withApprovalNote({
2917
3558
  type: "object",
2918
3559
  properties: {
2919
3560
  action: {
2920
3561
  type: "string",
2921
- description: "list=工坊里有哪些模块(默认);status=某个模块现在走到哪一步、下一步做什么;probe=在用户浏览器里探一条读;recon=先查 opencli 现成的能力(自带 adapter / 本机插件);open=只把它在这个项目上板(不装);install=装上并上板(第一次、会让用户点头)。",
2922
- enum: [...ACTIONS$1]
3562
+ description: "list=工坊里有哪些模块(默认);status=某个模块现在走到哪一步、下一步做什么;panel=这个工作区的面板上现在是什么样(谁读到了 / 谁读不到 / 谁崩过);probe=在用户浏览器里探一条读;recon=先查 opencli 现成的能力(自带 adapter / 本机插件);open=只把它在这个项目上板(不装);install=装上并上板(第一次、会让用户点头)。",
3563
+ enum: [...ACTIONS$2]
2923
3564
  },
2924
3565
  unit: {
2925
3566
  type: "string",
2926
- description: "status / open / install 填:工坊里那个模块的单元 id(list 会给)。"
3567
+ description: "status / panel / open / install 填:工坊里那个模块的单元 id(list 会给)。"
2927
3568
  },
2928
3569
  script: {
2929
3570
  type: "string",
@@ -2964,7 +3605,7 @@ function registerDiyTools(ctx, market, panels) {
2964
3605
  },
2965
3606
  required: ["action"],
2966
3607
  additionalProperties: false
2967
- },
3608
+ }, { required: true }),
2968
3609
  output: {
2969
3610
  schema: OUTPUT_SCHEMA$2,
2970
3611
  render: (_args, value) => {
@@ -2978,13 +3619,16 @@ function registerDiyTools(ctx, market, panels) {
2978
3619
  async execute(raw, exec) {
2979
3620
  const args = asRecord$3(raw) ?? {};
2980
3621
  const action = asText$1(args.action) || "list";
2981
- if (!ACTIONS$1.includes(action)) throw new Error(`不认识的 action:${action}(可用:${ACTIONS$1.join(" / ")})`);
3622
+ if (!ACTIONS$2.includes(action)) throw new Error(`不认识的 action:${action}(可用:${ACTIONS$2.join(" / ")})`);
2982
3623
  const packagesDir = workshopPackagesDir(ctx);
2983
3624
  if (packagesDir === null) throw new Error("算不出 DIY 工坊在哪(这台机器没给出 dshHomePath)—— 让用户跑一次 dsh-workbench setup");
2984
3625
  const session = sessionWorkspace(exec);
2985
3626
  if (!inWorkshopSession(ctx, session)) throw new Error(`这把工具只在 DIY 工坊里用:当前会话不在工坊里(这个会话的工作区 ${session ?? "(不知道)"})—— 普通项目里装模块用 workbench_modules,工坊的入口是左栏那一行「工作台DIY」`);
2986
3627
  const workspace = session;
2987
- const modules = workshopModules(packagesDir, profileModules(profileDirOf(ctx)).map((row) => row.unit));
3628
+ const installed = profileModules(profileDirOf(ctx)).map((row) => row.unit);
3629
+ const modules = workshopModules(packagesDir, installed);
3630
+ const registry = ctx.get(UNITS_SERVICE);
3631
+ const panelState = ctx.get(PANEL_STATE_SERVICE);
2988
3632
  if (action === "list") return plainJson({
2989
3633
  workshop: packagesDir,
2990
3634
  modules,
@@ -3003,7 +3647,12 @@ function registerDiyTools(ctx, market, panels) {
3003
3647
  name: one.name,
3004
3648
  installed: one.installed,
3005
3649
  known: panelEntries.some((entry) => entry.id === one.unit),
3006
- open: panels.isEnabled(one.unit, workspace)
3650
+ open: panels.isEnabled(one.unit, workspace),
3651
+ health: healthOf({
3652
+ registry,
3653
+ unit: one.unit,
3654
+ installed: one.installed
3655
+ })
3007
3656
  });
3008
3657
  if (action === "status") {
3009
3658
  const wanted = asText$1(args.unit);
@@ -3016,6 +3665,49 @@ function registerDiyTools(ctx, market, panels) {
3016
3665
  text: renderStatus(packagesDir, rows)
3017
3666
  });
3018
3667
  }
3668
+ if (action === "panel") {
3669
+ const snapshot = panelState?.snapshot(workspace) ?? {
3670
+ workspace,
3671
+ at: 0,
3672
+ units: []
3673
+ };
3674
+ const wanted = asText$1(args.unit);
3675
+ const board = panelEntries.filter((entry) => entry.shown && entry.hasCard).map((entry) => entry.id);
3676
+ const nameOf = new Map(modules.map((one) => [one.unit, one.name]));
3677
+ const reported = new Map(snapshot.units.map((one) => [one.unit, one]));
3678
+ const rows = [.../* @__PURE__ */ new Set([
3679
+ ...board,
3680
+ ...reported.keys(),
3681
+ ...nameOf.keys()
3682
+ ])].filter((unit) => wanted === "" || unit === wanted).map((unit) => {
3683
+ const report = reported.get(unit);
3684
+ return {
3685
+ unit,
3686
+ name: nameOf.get(unit) ?? "",
3687
+ health: healthOf({
3688
+ registry,
3689
+ unit,
3690
+ installed: installed.includes(unit)
3691
+ }),
3692
+ ...report === void 0 ? {} : { report }
3693
+ };
3694
+ });
3695
+ return plainJson({
3696
+ workshop: packagesDir,
3697
+ workspace,
3698
+ board,
3699
+ report: snapshot,
3700
+ panels: rows,
3701
+ text: renderPanel({
3702
+ packagesDir,
3703
+ workspace,
3704
+ rows,
3705
+ board,
3706
+ at: snapshot.at,
3707
+ now: Date.now()
3708
+ })
3709
+ });
3710
+ }
3019
3711
  if (action === "recon") {
3020
3712
  const keywords = keywordsOf(asText$1(args.keyword), asText$1(args.url));
3021
3713
  if (keywords.length === 0) throw new Error("recon 要一个关键词(站点名 / 系统名 / 域名都行):先拿它去 opencli 自带的那份目录里找有没有现成的能力,别一上来就自己写 adapter;用户不知道 opencli 是什么,这一步别问他。");
@@ -3907,11 +4599,47 @@ function createEnvDropper(options) {
3907
4599
  };
3908
4600
  }
3909
4601
  //#endregion
4602
+ //#region src/modules/panels/model/market-registry.ts
4603
+ async function rememberInstalledSource(deps, manager, pkg, registry) {
4604
+ const { registries, logger } = deps;
4605
+ if (registries === void 0 || registry === void 0) return;
4606
+ const scope = scopeOf(pkg);
4607
+ if (scope === null) return;
4608
+ const known = await manager.registries().catch(() => null);
4609
+ if (known !== null && sameRegistry(known.resolved, registry)) return;
4610
+ try {
4611
+ const added = registries.remember([{
4612
+ scope,
4613
+ registry
4614
+ }]);
4615
+ if (added.length > 0) logger?.info?.(`[dsh-workbench] 把 ${added.join("、")} 记进 profile 的 .npmrc(卸载只认 pnpm 的配置)`);
4616
+ } catch (error) {
4617
+ logger?.warn?.(`[dsh-workbench] 记这个 scope 的源失败:${String(error)}`);
4618
+ }
4619
+ }
4620
+ async function retryAfterRegistryFix(deps, manager, pkg, change) {
4621
+ const { registries, logger } = deps;
4622
+ if (registries === void 0 || !registryFailure(change)) return change;
4623
+ const known = await manager.registries().catch(() => null);
4624
+ const missing = registries.sources().filter((entry) => !sameRegistry(entry.registry, known?.resolved ?? null));
4625
+ if (missing.length === 0) return change;
4626
+ let added;
4627
+ try {
4628
+ added = registries.remember(missing);
4629
+ } catch (error) {
4630
+ logger?.warn?.(`[dsh-workbench] 卸 ${pkg} 撞上源,但补配置失败:${String(error)}`);
4631
+ return change;
4632
+ }
4633
+ if (added.length === 0) return change;
4634
+ logger?.warn?.(`[dsh-workbench] 卸 ${pkg} 撞上源:补 ${added.join("、")} 后重试一次`);
4635
+ return await manager.remove(pkg).catch(() => null) ?? change;
4636
+ }
4637
+ //#endregion
3910
4638
  //#region src/modules/panels/model/market.ts
3911
4639
  function createMarketService(options) {
3912
- const { manifests, manifestsNow, probe, shell, logger, onInstalled, onUninstalled, isReleased, bundles, installed, modules, registries, ledger, references } = options;
4640
+ const { manifests, manifestsNow, probe, shell, logger, onInstalled, onUninstalled, isReleased, bundles, installed, modules, workbench, registries, ledger, references } = options;
3913
4641
  const command = options.command ?? "opencli";
3914
- const catalog = () => (manifestsNow?.() ?? manifests).filter((manifest) => (manifest.env?.plugins ?? []).length > 0 || (manifest.env?.libs ?? []).length > 0);
4642
+ const catalog = () => manifestsNow?.() ?? manifests;
3915
4643
  function requireEntry(unit) {
3916
4644
  const manifest = catalog().find((item) => item.id === unit);
3917
4645
  if (manifest === void 0) throw new Error(`市场里没有这个模块:${unit}(可安装:${catalog().map((item) => item.id).join(", ") || "(空)"})`);
@@ -4054,44 +4782,16 @@ function createMarketService(options) {
4054
4782
  };
4055
4783
  }
4056
4784
  }
4057
- async function rememberInstalledSource(manager, pkg, registry) {
4058
- if (registries === void 0 || registry === void 0) return;
4059
- const scope = scopeOf(pkg);
4060
- if (scope === null) return;
4061
- const known = await manager.registries().catch(() => null);
4062
- if (known !== null && sameRegistry(known.resolved, registry)) return;
4063
- try {
4064
- const added = registries.remember([{
4065
- scope,
4066
- registry
4067
- }]);
4068
- if (added.length > 0) logger?.info?.(`[dsh-workbench] 把 ${added.join("、")} 记进 profile 的 .npmrc(卸载只认 pnpm 的配置)`);
4069
- } catch (error) {
4070
- logger?.warn?.(`[dsh-workbench] 记这个 scope 的源失败:${String(error)}`);
4071
- }
4072
- }
4073
- async function retryAfterRegistryFix(manager, pkg, change) {
4074
- if (registries === void 0 || !registryFailure(change)) return change;
4075
- const known = await manager.registries().catch(() => null);
4076
- const missing = registries.sources().filter((entry) => !sameRegistry(entry.registry, known?.resolved ?? null));
4077
- if (missing.length === 0) return change;
4078
- let added;
4079
- try {
4080
- added = registries.remember(missing);
4081
- } catch (error) {
4082
- logger?.warn?.(`[dsh-workbench] 卸 ${pkg} 撞上源,但补配置失败:${String(error)}`);
4083
- return change;
4084
- }
4085
- if (added.length === 0) return change;
4086
- logger?.warn?.(`[dsh-workbench] 卸 ${pkg} 撞上源:补 ${added.join("、")} 后重试一次`);
4087
- return await manager.remove(pkg).catch(() => null) ?? change;
4088
- }
4785
+ const registryFix = {
4786
+ ...registries === void 0 ? {} : { registries },
4787
+ ...logger === void 0 ? {} : { logger }
4788
+ };
4089
4789
  function routeDeps(manager) {
4090
4790
  return {
4091
4791
  manager,
4092
4792
  command,
4093
4793
  runAll,
4094
- rememberSource: rememberInstalledSource,
4794
+ rememberSource: (manager, pkg, registry) => rememberInstalledSource(registryFix, manager, pkg, registry),
4095
4795
  supplyAfter,
4096
4796
  ...shell === void 0 ? {} : { shell },
4097
4797
  ...installed === void 0 ? {} : { installed },
@@ -4237,7 +4937,7 @@ function createMarketService(options) {
4237
4937
  const before = unit === void 0 ? void 0 : await capabilityOf(unit, true);
4238
4938
  const first = await manager.remove(pkg);
4239
4939
  if (first === null) throw new Error("插件管理器回的卸载结果看不懂。");
4240
- const change = await retryAfterRegistryFix(manager, pkg, first);
4940
+ const change = await retryAfterRegistryFix(registryFix, manager, pkg, first);
4241
4941
  if (change.application !== "applied" || unit === void 0) return {
4242
4942
  change,
4243
4943
  keptFor: []
@@ -4273,6 +4973,24 @@ function createMarketService(options) {
4273
4973
  await service.update(unit).catch(() => void 0);
4274
4974
  return outcome;
4275
4975
  },
4976
+ async updateWorkbench(pkg) {
4977
+ const target = pkg.trim() === "" ? PLUGIN_ID : pkg.trim();
4978
+ if (target !== "@dsh-workbench/core") throw new Error(`这条路只更新工作台自己那一份(${PLUGIN_ID}):${target}`);
4979
+ const read = workbench?.(target);
4980
+ if (read === void 0) throw new Error(`读不到 profile 里装的 ${target} —— 没法判断装的是哪一份,更新做不了。`);
4981
+ if (read.local === true) throw new Error("这份工作台是从本仓装进来的(开发副本),不从源上更新。");
4982
+ const registry = read.registry;
4983
+ const latest = shell === void 0 || registry === void 0 ? void 0 : await latestVersion({
4984
+ shell,
4985
+ name: read.pkg,
4986
+ registry,
4987
+ force: true
4988
+ });
4989
+ const spec = latest === void 0 ? `${read.pkg}@latest` : `${read.pkg}@${latest}`;
4990
+ const outcome = await installDshBundle(routeDeps(requireBundles()), spec, { ...registry === void 0 ? {} : { registry } });
4991
+ logger?.info?.(`[dsh-workbench] 已更新工作台 ${read.version} → ${latest ?? "最新版"}`);
4992
+ return outcome;
4993
+ },
4276
4994
  async updatePlugin(pkg) {
4277
4995
  const outcome = await plugins.update(pkg);
4278
4996
  await reprobePlugin(pkg, outcome.name);
@@ -4337,6 +5055,11 @@ async function dispatchMarket(market, request) {
4337
5055
  action: "cancel-install",
4338
5056
  cancellation: await market.cancelInstall(request.requestId ?? "")
4339
5057
  };
5058
+ if (request.action === "update-workbench") return {
5059
+ ok: true,
5060
+ action: "update-workbench",
5061
+ change: (await market.updateWorkbench(request.name ?? "")).change
5062
+ };
4340
5063
  if (request.action === "update-plugin") {
4341
5064
  await market.updatePlugin(request.name ?? "");
4342
5065
  return {
@@ -4431,12 +5154,16 @@ function describePluginUninstall(pkg, outcome) {
4431
5154
  if (outcome.removed) return `已卸载 opencli 插件 ${pkg}:没人再用它,文件也删了。`;
4432
5155
  return `已从「你装的插件」里去掉 ${pkg}。${outcome.kept === void 0 ? "" : pluginKeptText(outcome.kept)}`;
4433
5156
  }
5157
+ function describeWorkbenchUpdate(change) {
5158
+ if (change.application === "failed" || change.application === "cancelled") return `没成功:${change.errorCode ?? change.application}`;
5159
+ return change.application === "restart-required" ? "工作台更新到新版了 —— 这一版要重启一次 dsh 才生效(重启由用户来,别自己重启)。" : "工作台更新到新版了 —— 客户端与宿主自己换上,不用重启。";
5160
+ }
4434
5161
  function usersOf(shared, names) {
4435
5162
  const hit = shared.find((item) => names.some((name) => name !== void 0 && name !== "" && item.id === `plugin:${name}`));
4436
5163
  if (hit === void 0 || hit.usedBy.length === 0) return "";
4437
5164
  return `它被 ${hit.usedBy.join("、")} 用着(动的是它们的供给,它们的状态跟着重探了一遍)`;
4438
5165
  }
4439
- var ACTIONS = [
5166
+ var ACTIONS$1 = [
4440
5167
  "list",
4441
5168
  "install",
4442
5169
  "update",
@@ -4444,7 +5171,8 @@ var ACTIONS = [
4444
5171
  "install-bundle",
4445
5172
  "update-dependency",
4446
5173
  "update-plugin",
4447
- "uninstall-plugin"
5174
+ "uninstall-plugin",
5175
+ "update-workbench"
4448
5176
  ];
4449
5177
  function registerMarketTools(ctx, market) {
4450
5178
  const tools = ctx.get("tools");
@@ -4454,14 +5182,14 @@ function registerMarketTools(ctx, market) {
4454
5182
  }
4455
5183
  const tool = {
4456
5184
  name: "workbench_modules",
4457
- description: "看 / 装 / 升 / 卸工作台的面板模块,以及用户自己装的 opencli 插件。install-bundle 按 spec(包名,可带版本;git / tarball / 本地路径也行)把模块包装进 profile —— 装完会连带装它声明的 opencli 插件;其余 action 按模块 id 动它要的外部供给;update-plugin / uninstall-plugin 按包名或插件名动用户装的那份插件。卸载只动工作台运行时,不影响终端那套;共用依赖只有升级。",
4458
- parameters: {
5185
+ description: "看 / 装 / 升 / 卸工作台的面板模块,以及用户自己装的 opencli 插件。install-bundle 按 spec(包名,可带版本;git / tarball / 本地路径也行)把模块包装进 profile —— 装完会连带装它声明的 opencli 插件;其余 action 按模块 id 动它要的外部供给;update-workbench 把工作台自己更新到源上的新版(客户端与宿主一般会自己换上;真要重启时结果里会说,重启由用户来,别自己重启)。卸载只动工作台运行时,不影响终端那套;共用依赖只有升级。",
5186
+ parameters: withApprovalNote({
4459
5187
  type: "object",
4460
5188
  properties: {
4461
5189
  action: {
4462
5190
  type: "string",
4463
- description: "install-bundle=按 spec 装模块包(要 spec),update-dependency=升级共用依赖(要 id),update-plugin / uninstall-plugin=动用户自己装的 opencli 插件(要 plugin),其余=对模块(要 id)。",
4464
- enum: [...ACTIONS]
5191
+ description: "install-bundle=按 spec 装模块包(要 spec),update-dependency=升级共用依赖(要 id),update-plugin / uninstall-plugin=动用户自己装的 opencli 插件(要 plugin),update-workbench=把工作台自己更新到新版(不用别的参数),其余=对模块(要 id)。",
5192
+ enum: [...ACTIONS$1]
4465
5193
  },
4466
5194
  id: {
4467
5195
  type: "string",
@@ -4482,7 +5210,7 @@ function registerMarketTools(ctx, market) {
4482
5210
  },
4483
5211
  required: ["action"],
4484
5212
  additionalProperties: false
4485
- },
5213
+ }, { required: true }),
4486
5214
  output: {
4487
5215
  schema: OUTPUT_SCHEMA$1,
4488
5216
  render: (_args, value) => {
@@ -4495,7 +5223,7 @@ function registerMarketTools(ctx, market) {
4495
5223
  },
4496
5224
  async execute(args) {
4497
5225
  const action = args?.action ?? "list";
4498
- if (!ACTIONS.includes(action)) throw new Error(`不认识的 action:${action}(可用:${ACTIONS.join(" / ")})`);
5226
+ if (!ACTIONS$1.includes(action)) throw new Error(`不认识的 action:${action}(可用:${ACTIONS$1.join(" / ")})`);
4499
5227
  if (action === "list") {
4500
5228
  const { modules, shared } = await market.list();
4501
5229
  return {
@@ -4516,6 +5244,13 @@ function registerMarketTools(ctx, market) {
4516
5244
  text: describeInstall(spec, outcome.module, outcome.supply, outcome.change)
4517
5245
  };
4518
5246
  }
5247
+ if (action === "update-workbench") {
5248
+ const outcome = await market.updateWorkbench("");
5249
+ return {
5250
+ change: outcome.change,
5251
+ text: describeWorkbenchUpdate(outcome.change)
5252
+ };
5253
+ }
4519
5254
  if (action === "update-plugin" || action === "uninstall-plugin") {
4520
5255
  const plugin = args?.plugin?.trim() ?? "";
4521
5256
  if (plugin === "") throw new Error(`${action} 需要一个 plugin:opencli 插件的包名或插件名 —— 就是市场「已安装 opencli 插件」那一段里的名字。`);
@@ -4566,53 +5301,70 @@ function registerMarketTools(ctx, market) {
4566
5301
  //#endregion
4567
5302
  //#region src/modules/panels/model/prefs.ts
4568
5303
  var PANELS_PREF_RESOURCE = "panels";
5304
+ var ACTIONS = [
5305
+ "show",
5306
+ "hide",
5307
+ "move",
5308
+ "reset",
5309
+ "follow"
5310
+ ];
4569
5311
  function parsePanelsPrefAction(payload) {
4570
- if (typeof payload !== "object" || payload === null) throw new Error("偏好写需要一个对象:{ action, id?, index?, path? }");
5312
+ if (typeof payload !== "object" || payload === null) throw new Error("偏好写需要一个对象:{ action, id?, index?, path?, scope? }");
4571
5313
  const value = payload;
4572
5314
  const action = value.action;
4573
- if (action !== "show" && action !== "hide" && action !== "enable" && action !== "disable" && action !== "move" && action !== "reset") throw new Error(`不认识的 action:${String(action)}(可用:show / hide / enable / disable / move / reset)`);
5315
+ if (typeof action !== "string" || !ACTIONS.includes(action)) throw new Error(`不认识的 action:${String(action)}(可用:${ACTIONS.join(" / ")})`);
5316
+ if (value.scope !== void 0 && value.scope !== "machine" && value.scope !== "workspace") throw new Error(`不认识的 scope:${String(value.scope)}(可用:workspace / machine)`);
4574
5317
  return {
4575
5318
  action,
4576
5319
  ...typeof value.id === "string" ? { id: value.id } : {},
4577
5320
  ...typeof value.index === "number" ? { index: value.index } : {},
4578
- ...typeof value.path === "string" ? { path: value.path } : {}
5321
+ ...typeof value.path === "string" ? { path: value.path } : {},
5322
+ ...value.scope === void 0 ? {} : { scope: value.scope }
4579
5323
  };
4580
5324
  }
4581
5325
  function applyPanelsPref(panels, payload) {
4582
5326
  const action = parsePanelsPrefAction(payload);
4583
5327
  const path = action.path ?? null;
5328
+ const scope = action.scope ?? "workspace";
5329
+ if (action.action === "follow") {
5330
+ if (scope === "machine") throw new Error("机器级默认板没有\"回到跟随\"一说:它就是那份默认本身(要回到的是\"没配过\")。");
5331
+ return {
5332
+ ok: true,
5333
+ units: action.id === void 0 ? panels.follow(path) : panels.follow(path, action.id)
5334
+ };
5335
+ }
4584
5336
  if (action.action === "reset") return {
4585
5337
  ok: true,
4586
- units: panels.reset(path)
5338
+ units: panels.reset(path, scope)
4587
5339
  };
4588
5340
  if (action.id === void 0) throw new Error(`${action.action} 需要一个 id(当前套件:${panels.list(path).map((entry) => entry.id).join(", ")})`);
4589
5341
  if (action.action === "show") return {
4590
5342
  ok: true,
4591
- units: panels.show(action.id, path)
5343
+ units: panels.show(action.id, path, scope)
4592
5344
  };
4593
5345
  if (action.action === "hide") return {
4594
5346
  ok: true,
4595
- units: panels.hide(action.id, path)
5347
+ units: panels.hide(action.id, path, scope)
4596
5348
  };
4597
5349
  return {
4598
5350
  ok: true,
4599
- units: panels.move(action.id, action.index ?? 0, path)
5351
+ units: panels.move(action.id, action.index ?? 0, path, scope)
4600
5352
  };
4601
5353
  }
4602
5354
  //#endregion
4603
5355
  //#region src/modules/panels/model/handoff.ts
4604
5356
  var PANELS_FACT = "panels";
4605
5357
  var CAPABILITY_FACT = "capability";
4606
- function textOf(value) {
5358
+ function textOf$1(value) {
4607
5359
  return typeof value === "string" ? value.trim() : "";
4608
5360
  }
4609
5361
  function actionOf(call) {
4610
- return textOf(call.args["action"]) || "list";
5362
+ return textOf$1(call.args["action"]) || "list";
4611
5363
  }
4612
5364
  function requestOf(call) {
4613
5365
  const action = actionOf(call);
4614
5366
  if (!PROVISION_ACTIONS.includes(action)) return void 0;
4615
- const target = textOf(call.args["unit"]) || textOf(call.args["id"]);
5367
+ const target = textOf$1(call.args["unit"]) || textOf$1(call.args["id"]);
4616
5368
  if (target === "") return void 0;
4617
5369
  const wanted = action;
4618
5370
  return isDependencyAction(wanted) ? {
@@ -4624,7 +5376,7 @@ function requestOf(call) {
4624
5376
  };
4625
5377
  }
4626
5378
  function panelsHandoffEntry(target = {}) {
4627
- const path = textOf(target.path);
5379
+ const path = textOf$1(target.path);
4628
5380
  return path === "" ? void 0 : {
4629
5381
  fact: PANELS_FACT,
4630
5382
  key: `path:${path}`
@@ -4635,6 +5387,7 @@ function panelsFactsOf(call, manifests) {
4635
5387
  if (call.source === "workbench_diy" && actionOf(call) === "list") return [PANELS_FACT];
4636
5388
  if (call.source !== "workbench_modules" && call.source !== "workbench_diy") return void 0;
4637
5389
  if (actionOf(call) === "list") return [PANELS_FACT, CAPABILITY_FACT];
5390
+ if (actionOf(call) === "update-workbench") return [PANELS_FACT, CAPABILITY_FACT];
4638
5391
  const request = requestOf(call);
4639
5392
  if (request === void 0) return void 0;
4640
5393
  return [
@@ -4668,9 +5421,11 @@ function createPanelsService(options) {
4668
5421
  return readBoard(folder) ?? EMPTY_BOARD;
4669
5422
  }
4670
5423
  function writeBoard(folder, next) {
5424
+ const order = next.order ?? [];
4671
5425
  folder.write(PANELS_FILE, {
4672
5426
  shown: next.shown,
4673
- hidden: next.hidden
5427
+ hidden: next.hidden,
5428
+ ...order.length === 0 ? {} : { order }
4674
5429
  });
4675
5430
  }
4676
5431
  function boardsOf(workspace) {
@@ -4701,6 +5456,9 @@ function createPanelsService(options) {
4701
5456
  if (next.length === 0) machine.remove(RELEASED_FILE);
4702
5457
  else machine.write(RELEASED_FILE, { ids: next });
4703
5458
  }
5459
+ function ownedSet() {
5460
+ return new Set(owned?.() ?? []);
5461
+ }
4704
5462
  function notify() {
4705
5463
  for (const listener of [...listeners]) try {
4706
5464
  listener();
@@ -4711,7 +5469,7 @@ function createPanelsService(options) {
4711
5469
  function withPersisted(folder, next, key) {
4712
5470
  writeBoard(folder, next);
4713
5471
  notify();
4714
- return resolvePanels(manifests(), boardsOf(key));
5472
+ return resolvePanels(manifests(), boardsOf(key), ownedSet());
4715
5473
  }
4716
5474
  function takeBackReleased(id) {
4717
5475
  const current = readReleased();
@@ -4722,18 +5480,31 @@ function createPanelsService(options) {
4722
5480
  const all = manifests();
4723
5481
  if (!all.some((manifest) => manifest.id === id)) throw new Error(`没有这个套件:${id}(可用:${all.map((manifest) => manifest.id).join(", ")})`);
4724
5482
  }
5483
+ function pluginRefs() {
5484
+ return options.plugins?.() ?? [];
5485
+ }
5486
+ function pluginIds() {
5487
+ return new Set(pluginRefs().map((plugin) => pluginPanelId(plugin.pkg)));
5488
+ }
4725
5489
  function requireSwitchable(id) {
5490
+ if (pluginIds().has(id)) return;
4726
5491
  requireKnown(id);
4727
- if (!manifests().some((manifest) => manifest.id === id && manifest.card !== null)) throw new Error(`${id} 没有卡片,不能收起(它是把别的套件放回来的入口)`);
5492
+ if (manifests().find((item) => item.id === id)?.card === null && ownedSet().has(id)) throw new Error(`${id} 没有卡片,不能收起(它是把别的套件放回来的入口)`);
4728
5493
  }
4729
5494
  function requireWorkspace(workspace) {
4730
5495
  const key = workspace?.trim() ?? "";
4731
5496
  if (key === "") throw new Error("这个会话不属于任何工作区:面板按工作区存,先打开或新建一个工作区");
4732
5497
  return key;
4733
5498
  }
5499
+ function visibleOrder(workspace) {
5500
+ return resolvePanels(manifests(), boardsOf(workspace), ownedSet()).filter((entry) => entry.shown && entry.hasCard).map((entry) => entry.id);
5501
+ }
4734
5502
  return {
4735
5503
  list(workspace, target) {
4736
- return resolvePanels(manifests(), target === "machine" ? boardsOf(null) : boardsOf(workspace));
5504
+ return resolvePanels(manifests(), target === "machine" ? boardsOf(null) : boardsOf(workspace), ownedSet());
5505
+ },
5506
+ listPlugins(workspace, target) {
5507
+ return resolvePluginPanels(pluginRefs(), target === "machine" ? boardsOf(null) : boardsOf(workspace));
4737
5508
  },
4738
5509
  boards(workspace) {
4739
5510
  return boardsOf(workspace);
@@ -4775,7 +5546,7 @@ function createPanelsService(options) {
4775
5546
  requireSwitchable(id);
4776
5547
  const { folder, key } = targetOf(target ?? "workspace", workspace);
4777
5548
  takeBackReleased(id);
4778
- return withPersisted(folder, withShown(boardIn(folder), id), key);
5549
+ return withPersisted(folder, withShown(boardIn(folder), id, key === null ? void 0 : visibleOrder(key)), key);
4779
5550
  },
4780
5551
  hide(id, workspace, target) {
4781
5552
  requireSwitchable(id);
@@ -4784,14 +5555,34 @@ function createPanelsService(options) {
4784
5555
  },
4785
5556
  move(id, index, workspace, target) {
4786
5557
  requireSwitchable(id);
4787
- const { folder, key } = targetOf(target ?? "workspace", workspace);
5558
+ const scope = target ?? "workspace";
5559
+ if (scope === "machine") throw new Error("机器级默认板没有顺序这一说:它只回答\"默认开哪几行\"。顺序是每个工作区自己的 —— 要排顺序就用默认作用域的那一次 move(或面板里拖动)。");
5560
+ const { folder, key } = targetOf(scope, workspace);
4788
5561
  const current = boardIn(folder);
4789
5562
  if (!current.shown.includes(id)) takeBackReleased(id);
4790
- return withPersisted(folder, withMoved(current, id, index), key);
5563
+ return withPersisted(folder, withMoved(current, id, index, visibleOrder(key)), key);
4791
5564
  },
4792
5565
  reset(workspace, target) {
4793
5566
  const { folder, key } = targetOf(target ?? "workspace", workspace);
4794
5567
  return withPersisted(folder, EMPTY_BOARD, key);
5568
+ },
5569
+ changedBy() {
5570
+ const counts = /* @__PURE__ */ new Map();
5571
+ for (const folder of store.scan(PANELS_MODULE_ID)) {
5572
+ const board = readBoard(folder);
5573
+ if (board === null) continue;
5574
+ for (const id of /* @__PURE__ */ new Set([...board.shown, ...board.hidden])) counts.set(id, (counts.get(id) ?? 0) + 1);
5575
+ }
5576
+ return counts;
5577
+ },
5578
+ follow(workspace, id) {
5579
+ const { folder, key } = targetOf("workspace", workspace);
5580
+ if (id === void 0) {
5581
+ folder.remove(PANELS_FILE);
5582
+ notify();
5583
+ return resolvePanels(manifests(), boardsOf(key), ownedSet());
5584
+ }
5585
+ return withPersisted(folder, withoutPanel(boardIn(folder), id), key);
4795
5586
  }
4796
5587
  };
4797
5588
  }
@@ -4802,13 +5593,15 @@ function panelRoutes(panels, values) {
4802
5593
  const path = param(search, "path") ?? null;
4803
5594
  const entry = panelsHandoffEntry(path === null ? {} : { path });
4804
5595
  const handed = entry === void 0 ? void 0 : values?.take(entry);
4805
- if (handed !== void 0) return {
4806
- ok: true,
4807
- units: handed.value
4808
- };
5596
+ const defaults = [...panels.list(null, "machine").filter((unit) => unit.shown).map((unit) => unit.id), ...panels.listPlugins(null, "machine").filter((plugin) => plugin.shown).map((plugin) => plugin.id)];
5597
+ const own = panels.boards(path).workspace !== null;
4809
5598
  return {
4810
5599
  ok: true,
4811
- units: panels.list(path)
5600
+ units: handed === void 0 ? panels.list(path) : handed.value,
5601
+ plugins: panels.listPlugins(path),
5602
+ defaults,
5603
+ own,
5604
+ changed: Object.fromEntries(panels.changedBy())
4812
5605
  };
4813
5606
  } };
4814
5607
  }
@@ -4867,6 +5660,18 @@ function createPluginLedger(options) {
4867
5660
  };
4868
5661
  }
4869
5662
  //#endregion
5663
+ //#region src/modules/panels/model/plugin-panel.ts
5664
+ function pluginPanelRefs(ledger) {
5665
+ return ledger.list().map((entry) => {
5666
+ const name = pluginNameOf(entry);
5667
+ return {
5668
+ pkg: entry.package,
5669
+ title: name ?? entry.package,
5670
+ ...name === void 0 ? {} : { name }
5671
+ };
5672
+ });
5673
+ }
5674
+ //#endregion
4870
5675
  //#region src/shared/api/installed-plugins.ts
4871
5676
  var INSTALLED_PLUGINS_ROUTE = "panels/plugins";
4872
5677
  //#endregion
@@ -4956,38 +5761,39 @@ function registerPanelTools(ctx, panels, values) {
4956
5761
  }
4957
5762
  const tool = {
4958
5763
  name: "workbench_panels",
4959
- description: "看或调右侧面板:启用哪些套件、什么顺序。启用/收起 = 卡片与 agent 工具一起上下线(别的项目不受影响,不删数据)。收起是\"这个工作区明确不要它\",连那些在机器级默认板上开着的套件也照样在这个工作区收起来,之后 show 能放回来。默认只动当前工作区(scope=workspace);scope=machine 动的是\"机器级默认板\" —— 机器级默认板只是还没配过的工作区的默认值,已经自己配过的工作区保持自己的选择,不会被它改。在那份板上 hide = 明确写下\"不要它\",这一笔比名单更管用:名单里还列着它也算不要,没配过的工作区因此看不到它;show 会把这笔划掉、把它放回去。",
4960
- parameters: {
5764
+ description: "看或调右侧面板:启用哪些套件、什么顺序。启用/收起 = 卡片与 agent 工具一起上下线(别的项目不受影响,不删数据)。收起是\"这个工作区明确不要它\",连那些在机器级默认板上开着的套件也照样在这个工作区收起来,之后 show 能放回来。顺序只属于工作区:没排过的按各模块自己声明的次序(开着的在前、关着的跟在后面);show 或 move 过就以那次为准。机器级默认板只回答\"默认开哪几行\",没有顺序这一说。scope=machine 动的是\"机器级默认板\" —— 它只是还没配过的工作区的默认值,已经自己配过的工作区保持自己的选择,不会被它改。在那份板上 hide = 明确写下\"不要它\",这一笔比名单更管用:名单里还列着它也算不要,没提过它的工作区因此看不到它;show 会把这笔划掉、把它放回去。本工作区那份板只记**改过的那几行**(逐行覆盖):没提到的行跟着机器级默认板走,所以那份板以后改了,没改过的工作区会跟着变。",
5765
+ parameters: withApprovalNote({
4961
5766
  type: "object",
4962
5767
  properties: {
4963
5768
  action: {
4964
5769
  type: "string",
4965
- description: "show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它(卡片与工具一起下线,默认在机器上也开着的套件同样收起,别的项目不受影响);move=调顺序;reset=这个工作区不再有自己的选择(之前收起、启用的都忘掉;它从此明确\"什么都不要\",机器级默认板也管不到它,要哪一块用 show 放回来)。",
5770
+ description: "show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它(卡片与工具一起下线,默认在机器上也开着的套件同样收起,别的项目不受影响);move=调这个工作区的顺序(新开一行时它排在最后);reset=清掉这个工作区改过的那些行(改完就全部回到跟着机器级默认板走);follow=把某一行放回跟随(带 id = 只放回那一行;不带 id = 全部放回)。",
4966
5771
  enum: [
4967
5772
  "list",
4968
5773
  "show",
4969
5774
  "hide",
4970
5775
  "move",
4971
- "reset"
5776
+ "reset",
5777
+ "follow"
4972
5778
  ]
4973
5779
  },
4974
5780
  id: {
4975
5781
  type: "string",
4976
- description: "show / hide / move 必填,如 coding-issues。"
5782
+ description: "show / hide / move 必填;follow 可选(带 id = 只把那一行放回跟随)。"
4977
5783
  },
4978
5784
  index: {
4979
5785
  type: "integer",
4980
- description: "move 的位置,0 基(0=最上面)。"
5786
+ description: "move 的位置,0 基(数的是当前开着的那几行)。"
4981
5787
  },
4982
5788
  scope: {
4983
5789
  type: "string",
4984
- description: "workspace(默认)=只动当前工作区;machine=动机器级默认板,那是还没配过的工作区的默认值 —— 已经自己配过的工作区保持自己的选择,不受它影响。",
5790
+ description: "workspace(默认)=只动当前工作区;machine=动机器级默认板,那是还没配过的工作区的默认值 —— 已经自己配过的工作区保持自己的选择,不受它影响。机器级板没有顺序这一说(move 会报错)。",
4985
5791
  enum: ["workspace", "machine"]
4986
5792
  }
4987
5793
  },
4988
5794
  required: ["action"],
4989
5795
  additionalProperties: false
4990
- },
5796
+ }, { required: true }),
4991
5797
  output: {
4992
5798
  schema: OUTPUT_SCHEMA,
4993
5799
  render: (_args, value) => {
@@ -5009,8 +5815,9 @@ function registerPanelTools(ctx, panels, values) {
5009
5815
  "show",
5010
5816
  "hide",
5011
5817
  "move",
5012
- "reset"
5013
- ].includes(action)) throw new Error(`不认识的 action:${action}(可用:list / show / hide / move / reset)`);
5818
+ "reset",
5819
+ "follow"
5820
+ ].includes(action)) throw new Error(`不认识的 action:${action}(可用:list / show / hide / move / reset / follow)`);
5014
5821
  if (action === "list") {
5015
5822
  const units = list();
5016
5823
  if (scope === "workspace") {
@@ -5022,11 +5829,19 @@ function registerPanelTools(ctx, panels, values) {
5022
5829
  text: render(units, workspace, scope)
5023
5830
  };
5024
5831
  }
5832
+ if (action === "follow") {
5833
+ if (scope === "machine") throw new Error("机器级默认板没有\"放回跟随\"一说:它就是那份默认本身,直接 show / hide 写它。");
5834
+ const units = panels.follow(workspace, id);
5835
+ return {
5836
+ units,
5837
+ text: id === void 0 ? `这个工作区改过的行全部清掉 —— 每一行都回到跟着机器级默认板走(那份板以后改了,这里跟着变)。\n${render(units, workspace, scope)}` : `${id} 放回跟随 —— 它不再由这个工作区定,改跟机器级默认板走。\n${render(units, workspace, scope)}`
5838
+ };
5839
+ }
5025
5840
  if (action === "reset") {
5026
5841
  const units = panels.reset(workspace, scope);
5027
5842
  return {
5028
5843
  units,
5029
- text: scope === "machine" ? `机器级默认板已清空(是"配成空",不是"没配过")——还没配过的工作区从此默认什么都不开。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `这个工作区不再有自己的面板选择(之前收起、启用的都忘掉了;它从此明确"什么都不要"—— 机器级默认板也管不到它,要哪一块用 show 放回来)。\n${render(units, workspace, scope)}`
5844
+ text: scope === "machine" ? `机器级默认板已清空 —— 从今往后,没在这一行改过的工作区默认什么都不开。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `这个工作区改过的行已全部清掉 —— 每一行都回到跟着机器级默认板走。\n${render(units, workspace, scope)}`
5030
5845
  };
5031
5846
  }
5032
5847
  if (id === void 0) throw new Error(`${action} 需要一个 id(例如 coding-issues)。当前套件:${list().map((entry) => entry.id).join(", ")}`);
@@ -5058,29 +5873,37 @@ function registerPanelTools(ctx, panels, values) {
5058
5873
  var OWNED = [{
5059
5874
  unit: panelsUnit.id,
5060
5875
  manifest: panelsUnit,
5061
- mount({ ctx, logger, manifests, ownedUnits, opencliCommand, probe, store, values }) {
5876
+ mount({ ctx, logger, manifests, ownedUnits, installable, opencliCommand, probe, store, values }) {
5877
+ const ledger = createPluginLedger({
5878
+ store,
5879
+ home: isolatedHome()
5880
+ });
5062
5881
  const panels = createPanelsService({
5063
5882
  manifests,
5064
5883
  store,
5065
5884
  owned: ownedUnits,
5885
+ plugins: () => pluginPanelRefs(ledger),
5066
5886
  logger
5067
5887
  });
5068
5888
  ctx.provide(PANELS_SERVICE, panels);
5069
5889
  const profileDir = profileDirOf(ctx);
5070
5890
  const shell = ctx.get("shell");
5071
- const ledger = createPluginLedger({
5072
- store,
5073
- home: isolatedHome()
5074
- });
5891
+ const moduleManifests = () => {
5892
+ const list = manifests();
5893
+ if (installable === void 0) return list.filter(hasEnvSupply);
5894
+ const ids = new Set(installable());
5895
+ return list.filter((manifest) => ids.has(manifest.id));
5896
+ };
5075
5897
  const market = createMarketService({
5076
- manifests: manifests(),
5077
- manifestsNow: manifests,
5898
+ manifests: moduleManifests(),
5899
+ manifestsNow: moduleManifests,
5078
5900
  ...probe === void 0 ? {} : { probe },
5079
5901
  shell,
5080
5902
  ...opencliCommand === void 0 ? {} : { command: opencliCommand },
5081
5903
  bundles: readBundleManager(ctx),
5082
5904
  installed: (pkg) => readInstalledModule(profileDir, pkg),
5083
5905
  modules: () => profileModules(profileDir),
5906
+ workbench: (pkg) => readProfilePackage(profileDir, pkg),
5084
5907
  registries: createRegistryNotebook(profileDir),
5085
5908
  ledger,
5086
5909
  references: () => {
@@ -5460,6 +6283,24 @@ function bundleRegistriesHandler(ctx) {
5460
6283
  };
5461
6284
  }
5462
6285
  var refusalLog = /* @__PURE__ */ new Map();
6286
+ async function workbenchVersion(input) {
6287
+ const read = readProfilePackage(input.dir, PLUGIN_ID);
6288
+ if (read === void 0) return void 0;
6289
+ const registry = read.registry ?? input.fallback;
6290
+ const queried = read.local === true || input.shell === void 0 || registry === void 0 ? void 0 : await latestVersion({
6291
+ shell: input.shell,
6292
+ name: read.pkg,
6293
+ registry,
6294
+ force: input.force
6295
+ });
6296
+ const latest = queried !== void 0 && isNewerVersion(queried, read.version) ? queried : void 0;
6297
+ return {
6298
+ pkg: read.pkg,
6299
+ installed: read.version,
6300
+ ...latest === void 0 ? {} : { latest },
6301
+ ...read.local === true ? { local: true } : {}
6302
+ };
6303
+ }
5463
6304
  function moduleVersionsHandler(ctx, shell) {
5464
6305
  return async (search) => {
5465
6306
  const manager = readBundleManager(ctx);
@@ -5472,36 +6313,44 @@ function moduleVersionsHandler(ctx, shell) {
5472
6313
  const refusals = new Map((ctx.get("workbenchUnits")?.refusals() ?? []).filter((one) => one.unit !== "").map((one) => [one.unit, one.reasons]));
5473
6314
  const force = param(search, "force") === "1";
5474
6315
  const fallback = (await manager.registries())?.resolved ?? void 0;
6316
+ const modules = await Promise.all(found.map(async (module) => {
6317
+ const registry = module.registry ?? fallback;
6318
+ const latest = shell === void 0 || registry === void 0 ? void 0 : await latestVersion({
6319
+ shell,
6320
+ name: module.pkg,
6321
+ registry,
6322
+ force
6323
+ });
6324
+ const raw = moduleDeclarationOf(dir, module.pkg);
6325
+ const declared = raw === void 0 ? [] : declarationProblemsOf(module.pkg, raw);
6326
+ const refused = refusals.get(module.unit) ?? (declared.length === 0 ? void 0 : declared);
6327
+ if (refused === void 0) refusalLog.delete(module.pkg);
6328
+ else {
6329
+ const line = refused.join(";");
6330
+ if (refusalLog.get(module.pkg) !== line) {
6331
+ refusalLog.set(module.pkg, line);
6332
+ ctx.logger?.warn?.(`[dsh-workbench] ${module.pkg} 装上了但用不了:${line}`);
6333
+ }
6334
+ }
6335
+ return {
6336
+ unit: module.unit,
6337
+ pkg: module.pkg,
6338
+ installed: module.version,
6339
+ ...latest === void 0 ? {} : { latest },
6340
+ ...module.local === true ? { local: true } : {},
6341
+ ...refused === void 0 ? {} : { refused }
6342
+ };
6343
+ }));
6344
+ const workbench = await workbenchVersion({
6345
+ dir,
6346
+ shell,
6347
+ fallback,
6348
+ force
6349
+ });
5475
6350
  return {
5476
6351
  ok: true,
5477
- modules: await Promise.all(found.map(async (module) => {
5478
- const registry = module.registry ?? fallback;
5479
- const latest = shell === void 0 || registry === void 0 ? void 0 : await latestVersion({
5480
- shell,
5481
- name: module.pkg,
5482
- registry,
5483
- force
5484
- });
5485
- const raw = moduleDeclarationOf(dir, module.pkg);
5486
- const declared = raw === void 0 ? [] : declarationProblemsOf(module.pkg, raw);
5487
- const refused = refusals.get(module.unit) ?? (declared.length === 0 ? void 0 : declared);
5488
- if (refused === void 0) refusalLog.delete(module.pkg);
5489
- else {
5490
- const line = refused.join(";");
5491
- if (refusalLog.get(module.pkg) !== line) {
5492
- refusalLog.set(module.pkg, line);
5493
- ctx.logger?.warn?.(`[dsh-workbench] ${module.pkg} 装上了但用不了:${line}`);
5494
- }
5495
- }
5496
- return {
5497
- unit: module.unit,
5498
- pkg: module.pkg,
5499
- installed: module.version,
5500
- ...latest === void 0 ? {} : { latest },
5501
- ...module.local === true ? { local: true } : {},
5502
- ...refused === void 0 ? {} : { refused }
5503
- };
5504
- }))
6352
+ modules,
6353
+ ...workbench === void 0 ? {} : { workbench }
5505
6354
  };
5506
6355
  };
5507
6356
  }
@@ -5528,15 +6377,26 @@ async function declarationRefusal(spec, inspection, ctx, manager) {
5528
6377
  });
5529
6378
  return refusedInspection(declarationRefusalOf(inspection.name, raw));
5530
6379
  }
5531
- async function packageRefusal(spec, inspection, ctx, manager) {
5532
- if (inspection.status !== "accepted") return void 0;
6380
+ async function packageOverride(spec, inspection, ctx, manager, asked) {
5533
6381
  const shell = ctx.get("shell");
5534
- const registry = typeof inspection.registry === "string" ? inspection.registry : (await manager.registries())?.resolved ?? void 0;
5535
- return (await packageRouteOf({
6382
+ const registry = (inspection.status === "accepted" && typeof inspection.registry === "string" ? inspection.registry : void 0) ?? (typeof asked === "string" ? asked : (await manager.registries())?.resolved ?? void 0);
6383
+ const route = await packageRouteOf({
5536
6384
  spec,
5537
6385
  ...shell === void 0 ? {} : { shell },
5538
6386
  ...registry === void 0 ? {} : { registry }
5539
- }))?.kind === "neither" ? {
6387
+ });
6388
+ if (route?.kind === "opencli-plugin") {
6389
+ if (inspection.status !== "refused") return void 0;
6390
+ const ref = splitNpmSpec(spec);
6391
+ return {
6392
+ status: "plugin",
6393
+ pkg: route.pkg ?? ref?.name ?? spec.trim(),
6394
+ registry: registry ?? null,
6395
+ ...route.pluginName === void 0 ? {} : { pluginName: route.pluginName },
6396
+ ...ref?.version === void 0 ? {} : { version: ref.version }
6397
+ };
6398
+ }
6399
+ return route?.kind === "neither" && inspection.status === "accepted" ? {
5540
6400
  status: "refused",
5541
6401
  problem: "not-a-module-or-plugin",
5542
6402
  reason: refusalReason()
@@ -5560,7 +6420,8 @@ function bundleInspectHandler(ctx) {
5560
6420
  message: "这个 profile 里没有插件管理器,装不了模块。"
5561
6421
  }
5562
6422
  };
5563
- const inspection = await manager.inspect(spec, registryOf(search));
6423
+ const asked = registryOf(search);
6424
+ const inspection = await manager.inspect(spec, asked);
5564
6425
  if (inspection === null) return {
5565
6426
  ok: false,
5566
6427
  error: {
@@ -5570,7 +6431,7 @@ function bundleInspectHandler(ctx) {
5570
6431
  };
5571
6432
  return {
5572
6433
  ok: true,
5573
- inspection: await packageRefusal(spec, inspection, ctx, manager) ?? await declarationRefusal(spec, inspection, ctx, manager) ?? inspection
6434
+ inspection: await packageOverride(spec, inspection, ctx, manager, asked) ?? await declarationRefusal(spec, inspection, ctx, manager) ?? inspection
5574
6435
  };
5575
6436
  };
5576
6437
  }
@@ -6275,6 +7136,28 @@ function withRunsRoute(result, unit) {
6275
7136
  }
6276
7137
  };
6277
7138
  }
7139
+ function notedRegistry(registry, writesOf) {
7140
+ return { register(definition) {
7141
+ if (!writesOf(definition.name)) return registry.register(definition);
7142
+ const noted = withApprovalNote(definition.parameters, { required: true });
7143
+ return registry.register(noted === definition.parameters ? definition : {
7144
+ ...definition,
7145
+ parameters: noted
7146
+ });
7147
+ } };
7148
+ }
7149
+ function scopedMountCtx(ctx, registry, writesOf) {
7150
+ const tools = notedRegistry(registry, writesOf);
7151
+ const get = (name) => {
7152
+ const value = ctx.get(name);
7153
+ return name === "tools" && value !== void 0 ? tools : value;
7154
+ };
7155
+ return new Proxy(ctx, { get(target, key) {
7156
+ if (key === "get") return get;
7157
+ const value = Reflect.get(target, key, target);
7158
+ return typeof value === "function" ? value.bind(target) : value;
7159
+ } });
7160
+ }
6278
7161
  function failureReasonOf(error) {
6279
7162
  if (error instanceof Error) return error.message;
6280
7163
  return typeof error === "string" ? error : String(error);
@@ -6418,6 +7301,9 @@ function mountUnits(units, deps) {
6418
7301
  continue;
6419
7302
  }
6420
7303
  const unit = host.manifest.id;
7304
+ const tools = deps.ctx.get("tools");
7305
+ const writes = toolUnits(deps.manifests());
7306
+ const mountCtx = tools === void 0 ? deps.ctx : scopedMountCtx(deps.ctx, tools, (name) => writes.get(name)?.writes === true);
6421
7307
  const longTasks = deps.longTasks === void 0 ? void 0 : createLongTaskHost({
6422
7308
  scope: deps.longTasks.scope,
6423
7309
  unit,
@@ -6427,10 +7313,11 @@ function mountUnits(units, deps) {
6427
7313
  try {
6428
7314
  result = host.face.mount({
6429
7315
  manifest: host.manifest,
6430
- ctx: deps.ctx,
7316
+ ctx: mountCtx,
6431
7317
  logger: deps.logger,
6432
7318
  manifests: deps.manifests,
6433
7319
  ownedUnits,
7320
+ ...deps.installable === void 0 ? {} : { installable: deps.installable },
6434
7321
  ...deps.opencliCommand === void 0 ? {} : { opencliCommand: deps.opencliCommand },
6435
7322
  ...deps.probe === void 0 ? {} : { probe: deps.probe },
6436
7323
  ...deps.values === void 0 ? {} : { values: deps.values },
@@ -6495,13 +7382,19 @@ function mountUnits(units, deps) {
6495
7382
  }
6496
7383
  //#endregion
6497
7384
  //#region src/shared/api/host/unit-registry.ts
7385
+ function installableUnitsOf(registry) {
7386
+ return registry.all().filter((host) => host.source.kind === "package").map((host) => host.unit);
7387
+ }
6498
7388
  function sourceLabel(source) {
6499
- return source.kind === "owned" ? "外壳自己" : source.pkg;
7389
+ if (source.kind === "owned") return "外壳自己";
7390
+ if (source.kind === "plugin") return "按已装 opencli 插件合成";
7391
+ return source.pkg;
6500
7392
  }
6501
7393
  function createUnitRegistry(options) {
6502
7394
  const refusals = [];
6503
7395
  const hosts = [];
6504
7396
  const byUnit = /* @__PURE__ */ new Map();
7397
+ const failures = /* @__PURE__ */ new Map();
6505
7398
  const listeners = /* @__PURE__ */ new Set();
6506
7399
  function notify() {
6507
7400
  for (const listener of listeners) listener();
@@ -6556,6 +7449,14 @@ function createUnitRegistry(options) {
6556
7449
  source: { kind: "owned" }
6557
7450
  });
6558
7451
  },
7452
+ registerPlugin(face) {
7453
+ return add({
7454
+ unit: face.manifest.id,
7455
+ manifest: face.manifest,
7456
+ face,
7457
+ source: { kind: "plugin" }
7458
+ });
7459
+ },
6559
7460
  all: () => hosts,
6560
7461
  owned: () => hosts.filter((host) => host.source.kind === "owned").map((host) => host.unit),
6561
7462
  refusals: () => refusals,
@@ -6563,7 +7464,14 @@ function createUnitRegistry(options) {
6563
7464
  subscribe(listener) {
6564
7465
  listeners.add(listener);
6565
7466
  return () => listeners.delete(listener);
6566
- }
7467
+ },
7468
+ fail(unit, reason) {
7469
+ failures.set(unit, reason);
7470
+ },
7471
+ recovered(unit) {
7472
+ failures.delete(unit);
7473
+ },
7474
+ failureOf: (unit) => failures.get(unit)
6567
7475
  };
6568
7476
  }
6569
7477
  //#endregion
@@ -6581,6 +7489,564 @@ function bindRosterFacts(options) {
6581
7489
  });
6582
7490
  }
6583
7491
  //#endregion
7492
+ //#region src/shared/api/host/plugin-catalog.ts
7493
+ function recordOf(value) {
7494
+ return typeof value === "object" && value !== null ? value : void 0;
7495
+ }
7496
+ function textOf(value) {
7497
+ return typeof value === "string" ? value : "";
7498
+ }
7499
+ function stringsOf(value) {
7500
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
7501
+ }
7502
+ function argsOf(raw) {
7503
+ if (!Array.isArray(raw)) return [];
7504
+ return raw.flatMap((item) => {
7505
+ const record = recordOf(item);
7506
+ if (record === void 0) return [];
7507
+ const name = textOf(record["name"]).trim();
7508
+ if (name === "") return [];
7509
+ const fallback = record["default"];
7510
+ return [{
7511
+ name,
7512
+ type: textOf(record["type"]) || "string",
7513
+ required: record["required"] === true,
7514
+ positional: record["positional"] === true,
7515
+ valueRequired: record["valueRequired"] === true,
7516
+ choices: stringsOf(record["choices"]),
7517
+ ...fallback === void 0 || fallback === null ? {} : { defaultValue: fallback },
7518
+ help: textOf(record["help"])
7519
+ }];
7520
+ });
7521
+ }
7522
+ function parsePluginRows(stdout) {
7523
+ const parsed = parseJsonLoose(stdout);
7524
+ if (!Array.isArray(parsed)) return void 0;
7525
+ const rows = [];
7526
+ for (const item of parsed) {
7527
+ const record = recordOf(item);
7528
+ if (record === void 0) return void 0;
7529
+ const name = textOf(record["name"]).trim();
7530
+ if (name === "") return void 0;
7531
+ rows.push({
7532
+ name,
7533
+ version: textOf(record["version"]),
7534
+ commands: stringsOf(record["commands"])
7535
+ });
7536
+ }
7537
+ return rows;
7538
+ }
7539
+ function parseCommandRows(stdout) {
7540
+ const parsed = parseJsonLoose(stdout);
7541
+ if (!Array.isArray(parsed)) return void 0;
7542
+ const rows = [];
7543
+ for (const item of parsed) {
7544
+ const record = recordOf(item);
7545
+ if (record === void 0) return void 0;
7546
+ const site = textOf(record["site"]).trim();
7547
+ const name = textOf(record["name"]).trim();
7548
+ if (site === "" || name === "") return void 0;
7549
+ rows.push({
7550
+ site,
7551
+ name,
7552
+ description: textOf(record["description"]),
7553
+ access: textOf(record["access"]),
7554
+ ...textOf(record["domain"]) === "" ? {} : { domain: textOf(record["domain"]) },
7555
+ args: argsOf(record["args"])
7556
+ });
7557
+ }
7558
+ return rows;
7559
+ }
7560
+ function commandOf(plugin, row) {
7561
+ return {
7562
+ plugin,
7563
+ name: row.name,
7564
+ description: row.description,
7565
+ access: row.access,
7566
+ args: row.args
7567
+ };
7568
+ }
7569
+ function attributeCommands(plugins, rows) {
7570
+ const bySite = /* @__PURE__ */ new Map();
7571
+ for (const row of rows) {
7572
+ const site = bySite.get(row.site) ?? /* @__PURE__ */ new Map();
7573
+ site.set(row.name, row);
7574
+ bySite.set(row.site, site);
7575
+ }
7576
+ const sets = [];
7577
+ const problems = [];
7578
+ if (plugins.length > 0 && rows.length === 0) problems.push(`opencli 报了 ${String(plugins.length)} 个插件,但命令面(opencli list -f json)里一条命令都没有 —— 两份清单对不上`);
7579
+ for (const plugin of plugins) {
7580
+ const found = bySite.get(plugin.name) ?? /* @__PURE__ */ new Map();
7581
+ if (found.size === 0) {
7582
+ problems.push(`「${plugin.name}」在 opencli 插件清单里,但命令面里没有一条 site=${plugin.name} 的命令 —— 归属不了,它这次不合成工具`);
7583
+ continue;
7584
+ }
7585
+ const declared = plugin.commands.length > 0 ? plugin.commands : [...found.keys()];
7586
+ const missing = declared.filter((name) => !found.has(name));
7587
+ if (missing.length > 0) problems.push(`「${plugin.name}」自己的清单里有 ${String(missing.length)} 条命令在命令面里找不到:${missing.join("、")}(插件版本旧或命令面没更新)`);
7588
+ const commands = declared.flatMap((name) => {
7589
+ const row = found.get(name);
7590
+ return row === void 0 ? [] : [commandOf(plugin.name, row)];
7591
+ });
7592
+ if (commands.length === 0) {
7593
+ problems.push(`「${plugin.name}」没有任何一条能归属的命令 —— 它这次不合成工具`);
7594
+ continue;
7595
+ }
7596
+ sets.push({
7597
+ plugin: plugin.name,
7598
+ version: plugin.version,
7599
+ commands
7600
+ });
7601
+ }
7602
+ return {
7603
+ sets,
7604
+ problems
7605
+ };
7606
+ }
7607
+ function fingerprintOf(plugins) {
7608
+ return plugins.map((plugin) => `${plugin.name}@${plugin.version}:${[...plugin.commands].sort().join(",")}`).sort().join("|");
7609
+ }
7610
+ function messageOf(error) {
7611
+ return error instanceof Error ? error.message : String(error);
7612
+ }
7613
+ function createPluginCatalog(options) {
7614
+ const timeoutMs = options.timeoutMs ?? 12e4;
7615
+ let current = {
7616
+ ok: false,
7617
+ at: 0,
7618
+ sets: [],
7619
+ problems: ["还没探过 opencli 的插件命令面"]
7620
+ };
7621
+ let heavy;
7622
+ async function read(line) {
7623
+ const shell = options.shell;
7624
+ if (shell === void 0) return {
7625
+ ok: false,
7626
+ problem: "没有执行缝(shell 服务不可用),探不了这个命令面"
7627
+ };
7628
+ try {
7629
+ const result = await runShell(shell, line, { timeoutMs });
7630
+ if (result.timedOut) return {
7631
+ ok: false,
7632
+ problem: `${line} 超过 ${String(Math.round(timeoutMs / 1e3))} 秒没返回`
7633
+ };
7634
+ if (result.exitCode !== 0) {
7635
+ const tail = result.stderr.trim() || result.stdout.trim();
7636
+ return {
7637
+ ok: false,
7638
+ problem: `${line} 退出码 ${String(result.exitCode)}${tail === "" ? "" : `:${tail.split("\n").slice(-3).join(" ")}`}`
7639
+ };
7640
+ }
7641
+ return {
7642
+ ok: true,
7643
+ stdout: result.stdout
7644
+ };
7645
+ } catch (error) {
7646
+ return {
7647
+ ok: false,
7648
+ problem: `${line} 没跑起来:${messageOf(error)}`
7649
+ };
7650
+ }
7651
+ }
7652
+ function fail(problem) {
7653
+ current = {
7654
+ ok: false,
7655
+ at: Date.now(),
7656
+ sets: current.sets,
7657
+ problems: [problem]
7658
+ };
7659
+ return current;
7660
+ }
7661
+ async function refresh(callOptions = {}) {
7662
+ const pluginsRun = await read(`${options.command} plugin list -f json`);
7663
+ if (!pluginsRun.ok) return fail(pluginsRun.problem);
7664
+ const plugins = parsePluginRows(pluginsRun.stdout);
7665
+ if (plugins === void 0) return fail("opencli plugin list -f json 的输出读不懂(不是带 name 的 JSON 数组)");
7666
+ const fingerprint = fingerprintOf(plugins);
7667
+ if (!callOptions.force && heavy !== void 0 && heavy.fingerprint === fingerprint) {
7668
+ current = {
7669
+ ok: true,
7670
+ at: Date.now(),
7671
+ sets: heavy.sets,
7672
+ problems: heavy.problems
7673
+ };
7674
+ return current;
7675
+ }
7676
+ const commandsRun = await read(`${options.command} list -f json`);
7677
+ if (!commandsRun.ok) return fail(commandsRun.problem);
7678
+ const rows = parseCommandRows(commandsRun.stdout);
7679
+ if (rows === void 0) return fail("opencli list -f json 的输出读不懂(不是带 site / name 的 JSON 数组)");
7680
+ const attributed = attributeCommands(plugins, rows);
7681
+ heavy = {
7682
+ fingerprint,
7683
+ sets: attributed.sets,
7684
+ problems: attributed.problems
7685
+ };
7686
+ current = {
7687
+ ok: true,
7688
+ at: Date.now(),
7689
+ sets: attributed.sets,
7690
+ problems: attributed.problems
7691
+ };
7692
+ return current;
7693
+ }
7694
+ return {
7695
+ refresh,
7696
+ snapshot: () => current
7697
+ };
7698
+ }
7699
+ //#endregion
7700
+ //#region src/shared/api/host/plugin-login-hint.ts
7701
+ var NOT_LOGGED_IN = /未登录|请先登录|not logged in/i;
7702
+ function propertyOf$1(error, key) {
7703
+ return typeof error === "object" && error !== null ? error[key] : void 0;
7704
+ }
7705
+ function helpUrlOfError(error) {
7706
+ const raw = propertyOf$1(error, "helpUrl");
7707
+ if (typeof raw !== "string") return void 0;
7708
+ const trimmed = raw.trim();
7709
+ return trimmed === "" ? void 0 : trimmed;
7710
+ }
7711
+ function isAuthFailure(error, message) {
7712
+ if (NOT_LOGGED_IN.test(message)) return true;
7713
+ return propertyOf$1(error, "code") === "auth-required";
7714
+ }
7715
+ function loginUrlOfDomain(domain) {
7716
+ const bare = (domain ?? "").trim().replace(/\/+$/, "");
7717
+ if (bare === "" || bare.includes("*")) return void 0;
7718
+ return /^https?:\/\//.test(bare) ? bare : `https://${bare}/login`;
7719
+ }
7720
+ function pluginLoginHint(error, domain) {
7721
+ const message = error instanceof Error ? error.message : String(error);
7722
+ if (!isAuthFailure(error, message)) return message;
7723
+ const url = helpUrlOfError(error) ?? loginUrlOfDomain(domain);
7724
+ if (url !== void 0) return `${message}\n在 opencli 那扇浏览器窗里打开 ${url} 登一次,然后重试。`;
7725
+ return `${message}\n在 opencli 那扇浏览器窗里登录这个站点(插件没给出具体地址,工作台拼不出来)—— 或跑一次工作台的体检/预热,然后重试。`;
7726
+ }
7727
+ //#endregion
7728
+ //#region src/shared/api/host/plugin-tools.ts
7729
+ var SCHEMA_TYPE = {
7730
+ int: "integer",
7731
+ integer: "integer",
7732
+ number: "number",
7733
+ float: "number",
7734
+ bool: "boolean",
7735
+ boolean: "boolean",
7736
+ string: "string"
7737
+ };
7738
+ var KEY_ARG_LIMIT = 3;
7739
+ function pluginToolName(plugin, command) {
7740
+ return `${plugin}_${command}`.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/_{2,}/g, "_").replace(/^_+|_+$/g, "");
7741
+ }
7742
+ function schemaTypeOf(arg) {
7743
+ return SCHEMA_TYPE[arg.type.toLowerCase()] ?? "string";
7744
+ }
7745
+ function propertyOf(arg) {
7746
+ const type = schemaTypeOf(arg);
7747
+ const enumValues = arg.choices.length > 0 ? arg.choices.map((choice) => choice) : void 0;
7748
+ return {
7749
+ type,
7750
+ ...arg.help.trim() === "" ? {} : { description: arg.help.trim() },
7751
+ ...enumValues === void 0 ? {} : { enum: enumValues },
7752
+ ...arg.defaultValue === void 0 ? {} : { default: arg.defaultValue }
7753
+ };
7754
+ }
7755
+ function pluginToolParameters(command) {
7756
+ const properties = {};
7757
+ for (const arg of command.args) properties[arg.name] = propertyOf(arg);
7758
+ const required = command.args.filter((arg) => arg.required).map((arg) => arg.name);
7759
+ const schema = {
7760
+ type: "object",
7761
+ properties,
7762
+ ...required.length === 0 ? {} : { required },
7763
+ additionalProperties: false
7764
+ };
7765
+ return pluginToolWrites(command) ? withApprovalNote(schema, { required: true }) : schema;
7766
+ }
7767
+ function pluginToolDescription(command) {
7768
+ const described = command.description.trim();
7769
+ return described === "" ? `${command.plugin} ${command.name}(opencli 命令)` : described;
7770
+ }
7771
+ function pluginToolWrites(command) {
7772
+ return command.access !== "read";
7773
+ }
7774
+ function valuesOf(raw) {
7775
+ return typeof raw === "object" && raw !== null ? raw : {};
7776
+ }
7777
+ function pluginToolArgv(command, raw) {
7778
+ const values = valuesOf(raw);
7779
+ const argv = [];
7780
+ for (const arg of command.args) {
7781
+ if (!arg.positional) continue;
7782
+ const value = values[arg.name];
7783
+ if (value === void 0 || value === null) continue;
7784
+ argv.push(String(value));
7785
+ }
7786
+ for (const arg of command.args) {
7787
+ if (arg.positional) continue;
7788
+ const value = values[arg.name];
7789
+ if (value === void 0 || value === null) continue;
7790
+ if (typeof value === "boolean") argv.push(`--${arg.name}`, value ? "true" : "false");
7791
+ else if (Array.isArray(value)) argv.push(`--${arg.name}`, value.map((item) => String(item)).join(","));
7792
+ else argv.push(`--${arg.name}`, String(value));
7793
+ }
7794
+ return argv;
7795
+ }
7796
+ function pluginKeyArgs(command, raw) {
7797
+ const values = valuesOf(raw);
7798
+ const ordered = [...command.args.filter((arg) => arg.required), ...command.args.filter((arg) => !arg.required)];
7799
+ const parts = [];
7800
+ for (const arg of ordered) {
7801
+ const value = values[arg.name];
7802
+ if (value === void 0 || value === null || value === "") continue;
7803
+ parts.push(`${arg.name}=${String(value)}`);
7804
+ if (parts.length === KEY_ARG_LIMIT) break;
7805
+ }
7806
+ return parts.length === 0 ? "没有带参数" : parts.join("、");
7807
+ }
7808
+ function pluginApprovalQuestion(command, raw) {
7809
+ const key = pluginKeyArgs(command, raw);
7810
+ return {
7811
+ zh: `允许执行 ${command.plugin} 的 ${command.name}(${key})吗?`,
7812
+ en: `Run ${command.plugin} ${command.name} (${key})?`
7813
+ };
7814
+ }
7815
+ function pluginApprovalFor(command) {
7816
+ const denied = command.access === "read";
7817
+ return (call) => {
7818
+ if (denied) return { kind: "read" };
7819
+ const question = pluginApprovalQuestion(command, call.arguments);
7820
+ return {
7821
+ kind: "ask",
7822
+ reason: command.access === "write" ? `${command.plugin} ${command.name} 在 opencli 里标着 write` : `${command.plugin} ${command.name} 没有拿到 access(fail-safe:按写处理)`,
7823
+ question,
7824
+ fallback: question
7825
+ };
7826
+ };
7827
+ }
7828
+ //#endregion
7829
+ //#region src/shared/api/host/plugin-units.ts
7830
+ var TOOL_OUTPUT_SCHEMA = {
7831
+ type: "object",
7832
+ properties: {},
7833
+ additionalProperties: true
7834
+ };
7835
+ function signatureOf(version, commands) {
7836
+ return [version, ...commands.map((command) => `${command.name}:${command.access}:${command.args.map((arg) => `${arg.name}${arg.required ? "!" : ""}`).join("+")}`)].join("|");
7837
+ }
7838
+ function toolsOf(plugin, commands, taken) {
7839
+ const tools = [];
7840
+ const problems = [];
7841
+ const claimed = /* @__PURE__ */ new Set();
7842
+ for (const command of commands) {
7843
+ const name = pluginToolName(plugin, command.name);
7844
+ if (taken.has(name) || claimed.has(name)) {
7845
+ problems.push(`工具名 ${name}(${plugin} ${command.name})已经被占着 —— 这条命令这次不合成(先来的那份说了算)`);
7846
+ continue;
7847
+ }
7848
+ claimed.add(name);
7849
+ tools.push({
7850
+ name,
7851
+ command
7852
+ });
7853
+ }
7854
+ return {
7855
+ tools,
7856
+ problems
7857
+ };
7858
+ }
7859
+ function planOf(options, registered) {
7860
+ const sets = new Map(options.catalog.snapshot().sets.map((set) => [set.plugin, set]));
7861
+ const mine = new Set([...registered.values()].flatMap((item) => item.tools));
7862
+ const taken = new Set([...options.toolNames?.() ?? []].filter((name) => !mine.has(name)));
7863
+ const units = [];
7864
+ const rows = [];
7865
+ const problems = [];
7866
+ for (const row of options.rows()) {
7867
+ const plugin = row.name?.trim() ?? "";
7868
+ if (plugin === "") {
7869
+ problems.push(`面板上的插件行 ${row.id}(${row.pkg})没有 opencli 插件名 —— 认不出它,这次不给它合成工具`);
7870
+ continue;
7871
+ }
7872
+ const set = sets.get(plugin);
7873
+ if (set === void 0) {
7874
+ problems.push(`已装插件「${plugin}」在 opencli 的插件清单 / 命令面里找不到 —— 归属不了,这次不给它合成工具`);
7875
+ continue;
7876
+ }
7877
+ sets.delete(plugin);
7878
+ const declared = options.declared(plugin);
7879
+ const overlapped = set.commands.filter((command) => declared.has(command.name));
7880
+ if (overlapped.length > 0) problems.push(`「${plugin}」有 ${overlapped.length} 条命令由已装模块自己提供(${overlapped.map((command) => command.name).join("、")})—— 通用桥不重复合成`);
7881
+ const commands = set.commands.filter((command) => !declared.has(command.name));
7882
+ const built = toolsOf(plugin, commands, taken);
7883
+ problems.push(...built.problems);
7884
+ for (const tool of built.tools) taken.add(tool.name);
7885
+ if (built.tools.length === 0 && commands.length === set.commands.length) problems.push(`「${plugin}」的命令一条都没能合成工具`);
7886
+ rows.push({
7887
+ plugin,
7888
+ pkg: row.pkg,
7889
+ version: set.version,
7890
+ commands: set.commands.length,
7891
+ synthesized: built.tools.length,
7892
+ tools: built.tools.map((tool) => tool.name)
7893
+ });
7894
+ if (built.tools.length === 0) continue;
7895
+ units.push({
7896
+ id: row.id,
7897
+ plugin,
7898
+ pkg: row.pkg,
7899
+ version: set.version,
7900
+ signature: signatureOf(set.version, commands),
7901
+ commands,
7902
+ tools: built.tools
7903
+ });
7904
+ }
7905
+ for (const set of sets.values()) problems.push(`opencli 里装着「${set.plugin}」(${String(set.commands.length)} 条命令),但它不在插件台账里(不是从市场装的)—— 面板上没有它的开关,它的命令这次不进工具面`);
7906
+ return {
7907
+ units,
7908
+ rows,
7909
+ problems
7910
+ };
7911
+ }
7912
+ function faceOf(unit, opencliFor) {
7913
+ const byTool = new Map(unit.tools.map((tool) => [tool.name, tool.command]));
7914
+ const manifest = {
7915
+ id: unit.id,
7916
+ card: null,
7917
+ tools: unit.tools.map((tool) => ({
7918
+ name: tool.name,
7919
+ writes: pluginToolWrites(tool.command)
7920
+ })),
7921
+ host: { inject: [] }
7922
+ };
7923
+ return {
7924
+ unit: unit.id,
7925
+ manifest,
7926
+ mount(deps) {
7927
+ const tools = deps.ctx.get("tools");
7928
+ if (tools === void 0) {
7929
+ deps.logger?.info?.(`[dsh-workbench] 没有 tools 服务,跳过 ${unit.plugin} 的命令工具注册`);
7930
+ return {
7931
+ routes: {},
7932
+ registerTools: () => []
7933
+ };
7934
+ }
7935
+ const opencli = opencliFor(unit.plugin);
7936
+ const definitions = unit.tools.map((tool) => ({
7937
+ name: tool.name,
7938
+ description: pluginToolDescription(tool.command),
7939
+ parameters: pluginToolParameters(tool.command),
7940
+ output: {
7941
+ schema: TOOL_OUTPUT_SCHEMA,
7942
+ render: (_args, value) => textOutput(jsonText(value))
7943
+ },
7944
+ async execute(raw, exec) {
7945
+ const workspace = sessionWorkspace(exec);
7946
+ try {
7947
+ return (await opencli.json([tool.command.name, ...pluginToolArgv(tool.command, raw)], workspace === null ? {} : { workdir: workspace })).value;
7948
+ } catch (error) {
7949
+ throw new Error(pluginLoginHint(error, tool.command.domain), { cause: error });
7950
+ }
7951
+ }
7952
+ }));
7953
+ return {
7954
+ routes: {},
7955
+ factsOf: () => [],
7956
+ registerTools: () => definitions.map((definition) => tools.register(definition)),
7957
+ needsApproval: (call) => {
7958
+ const command = call.name === void 0 ? void 0 : byTool.get(call.name);
7959
+ return command === void 0 ? void 0 : pluginApprovalFor(command)(call);
7960
+ }
7961
+ };
7962
+ }
7963
+ };
7964
+ }
7965
+ function reconcile(registered, units, options) {
7966
+ const wanted = new Map(units.map((unit) => [unit.id, unit]));
7967
+ for (const [id, item] of [...registered]) {
7968
+ const next = wanted.get(id);
7969
+ if (next !== void 0 && next.signature === item.signature) continue;
7970
+ item.drop();
7971
+ registered.delete(id);
7972
+ }
7973
+ for (const unit of units) {
7974
+ if (registered.has(unit.id)) continue;
7975
+ registered.set(unit.id, {
7976
+ signature: unit.signature,
7977
+ tools: unit.tools.map((tool) => tool.name),
7978
+ drop: options.registry.registerPlugin(faceOf(unit, options.opencliFor))
7979
+ });
7980
+ }
7981
+ }
7982
+ function createPluginUnits(options) {
7983
+ const registered = /* @__PURE__ */ new Map();
7984
+ let report = {
7985
+ ok: false,
7986
+ at: 0,
7987
+ plugins: [],
7988
+ problems: ["还没探过 opencli 的插件命令面"]
7989
+ };
7990
+ let inflight;
7991
+ let lastTouch = 0;
7992
+ let warned = "";
7993
+ function warn(problems) {
7994
+ const text = [...problems].sort().join("\n");
7995
+ if (text === "" || text === warned) return;
7996
+ warned = text;
7997
+ options.logger?.warn?.(`[dsh-workbench] opencli 插件命令面:\n${text}`);
7998
+ }
7999
+ async function run(force) {
8000
+ const probed = await options.catalog.refresh(force ? { force: true } : {});
8001
+ if (!probed.ok) {
8002
+ const problems = [...probed.problems, "这次探测没成功:上面这些插件行的工具面维持在上一份(不是\"这些插件没有命令\")"];
8003
+ report = {
8004
+ ok: false,
8005
+ at: Date.now(),
8006
+ plugins: report.plugins,
8007
+ problems
8008
+ };
8009
+ warn(problems);
8010
+ return report;
8011
+ }
8012
+ const plan = planOf(options, registered);
8013
+ reconcile(registered, plan.units, options);
8014
+ report = {
8015
+ ok: true,
8016
+ at: Date.now(),
8017
+ plugins: plan.rows,
8018
+ problems: plan.problems
8019
+ };
8020
+ warn(plan.problems);
8021
+ return report;
8022
+ }
8023
+ function refresh(callOptions = {}) {
8024
+ if (inflight !== void 0) return inflight;
8025
+ const task = run(callOptions.force === true).finally(() => {
8026
+ inflight = void 0;
8027
+ });
8028
+ inflight = task;
8029
+ return task;
8030
+ }
8031
+ return {
8032
+ refresh,
8033
+ touch() {
8034
+ const now = Date.now();
8035
+ if (inflight !== void 0) return;
8036
+ if (now - lastTouch < 3e4) return;
8037
+ lastTouch = now;
8038
+ refresh().catch((error) => {
8039
+ options.logger?.warn?.(`[dsh-workbench] 探 opencli 插件命令面时抛了错:${String(error)}`);
8040
+ });
8041
+ },
8042
+ report: () => report,
8043
+ dispose() {
8044
+ for (const item of registered.values()) item.drop();
8045
+ registered.clear();
8046
+ }
8047
+ };
8048
+ }
8049
+ //#endregion
6584
8050
  //#region src/shared/api/host/tool-scope.ts
6585
8051
  function agentOf(value) {
6586
8052
  return typeof value === "object" && value !== null ? value : void 0;
@@ -6696,6 +8162,7 @@ function seedWorkbenchRuntime(ctx) {
6696
8162
  function apply(ctx) {
6697
8163
  const facts = createFactLog();
6698
8164
  const values = createValueHandoff();
8165
+ const panelState = createPanelStateStore();
6699
8166
  seedWorkbenchRuntime(ctx);
6700
8167
  const opencli = opencliCommandOf(ctx);
6701
8168
  const routes = {
@@ -6708,23 +8175,17 @@ function apply(ctx) {
6708
8175
  };
6709
8176
  const renderers = /* @__PURE__ */ new Map();
6710
8177
  const shell = ctx.get("shell");
6711
- let roster;
6712
8178
  const profileDir = profileDirOf(ctx);
6713
8179
  const units = createUnitRegistry({
6714
- declarationOf: (unit) => {
6715
- if (roster === void 0) roster = new Map(profileModules(profileDir).map((row) => [row.unit, row.pkg]));
6716
- const pkg = roster.get(unit);
6717
- return pkg === void 0 ? void 0 : {
6718
- pkg,
6719
- declaration: moduleDeclarationOf(profileDir, pkg)
6720
- };
6721
- },
8180
+ declarationOf: createDeclarationLookup(profileDir),
6722
8181
  logger: ctx.logger
6723
8182
  });
6724
8183
  ctx.provide(UNITS_SERVICE, units);
8184
+ ctx.provide(PANEL_STATE_SERVICE, panelState);
6725
8185
  routes[UNIT_MANIFESTS_ROUTE] = () => ({
6726
8186
  ok: true,
6727
- manifests: units.manifests()
8187
+ manifests: units.manifests(),
8188
+ installable: installableUnitsOf(units)
6728
8189
  });
6729
8190
  assembleHostUnits(units);
6730
8191
  ctx.inject(["workspaceRegistry"], (scoped) => {
@@ -6767,6 +8228,23 @@ function apply(ctx) {
6767
8228
  deniedTools: (workspace) => deniedNow(workspace),
6768
8229
  logger: ctx.logger
6769
8230
  });
8231
+ const pluginTools = shell === void 0 ? void 0 : createPluginUnits({
8232
+ registry: units,
8233
+ catalog: createPluginCatalog({
8234
+ shell,
8235
+ command: opencli.command
8236
+ }),
8237
+ rows: () => ctx.get("workbenchPanels")?.listPlugins(null) ?? [],
8238
+ declared: (plugin) => declaredPluginCommands(profileDir).get(plugin) ?? /* @__PURE__ */ new Set(),
8239
+ opencliFor: (plugin) => createOpencli({
8240
+ shell,
8241
+ plugin,
8242
+ command: opencli.command,
8243
+ logger: ctx.logger
8244
+ }),
8245
+ toolNames: () => new Set(unitsOfTool().keys()),
8246
+ logger: ctx.logger
8247
+ });
6770
8248
  const mounted = /* @__PURE__ */ new Map();
6771
8249
  const mountFailures = /* @__PURE__ */ new Map();
6772
8250
  const factsOfMap = /* @__PURE__ */ new Map();
@@ -6781,6 +8259,7 @@ function apply(ctx) {
6781
8259
  if (item === void 0) return;
6782
8260
  mounted.delete(id);
6783
8261
  mountFailures.delete(id);
8262
+ units.recovered(id);
6784
8263
  for (const dispose of item.toolDisposers) dispose?.();
6785
8264
  item.gateOff?.();
6786
8265
  item.plan.disposeLongTasks();
@@ -6799,6 +8278,7 @@ function apply(ctx) {
6799
8278
  logger: ctx.logger,
6800
8279
  manifests: liveManifests,
6801
8280
  ownedUnits: () => units.owned(),
8281
+ installable: () => installableUnitsOf(units),
6802
8282
  store,
6803
8283
  opencliCommand: opencli.command,
6804
8284
  probe: (unit, options) => probeOne(unit, options?.force ?? false),
@@ -6813,6 +8293,7 @@ function apply(ctx) {
6813
8293
  reason: failureReasonOf(error),
6814
8294
  host
6815
8295
  });
8296
+ units.fail(id, failureReasonOf(error));
6816
8297
  ctx.logger?.warn?.(`[dsh-workbench] ${id} 套件起不来(装配期):${failureReasonOf(error)}`);
6817
8298
  return;
6818
8299
  }
@@ -6845,6 +8326,7 @@ function apply(ctx) {
6845
8326
  gateOff: one.gate?.subscribe(() => toolScope.reapply())
6846
8327
  });
6847
8328
  mountFailures.delete(id);
8329
+ units.recovered(id);
6848
8330
  } catch (error) {
6849
8331
  for (const route of added) delete routes[route];
6850
8332
  for (const kind of one.contextRenderers.keys()) renderers.delete(kind);
@@ -6856,6 +8338,7 @@ function apply(ctx) {
6856
8338
  reason: failureReasonOf(error),
6857
8339
  host
6858
8340
  });
8341
+ units.fail(id, failureReasonOf(error));
6859
8342
  ctx.logger?.warn?.(`[dsh-workbench] ${id} 套件起不来(注册期):${failureReasonOf(error)}`);
6860
8343
  }
6861
8344
  }
@@ -6880,7 +8363,10 @@ function apply(ctx) {
6880
8363
  function sync() {
6881
8364
  const present = new Set(units.all().map((one) => one.unit));
6882
8365
  for (const id of [...mounted.keys()]) if (!present.has(id)) unmountUnit(id);
6883
- for (const id of [...mountFailures.keys()]) if (!present.has(id)) mountFailures.delete(id);
8366
+ for (const id of [...mountFailures.keys()]) if (!present.has(id)) {
8367
+ mountFailures.delete(id);
8368
+ units.recovered(id);
8369
+ }
6884
8370
  for (const host of units.all()) {
6885
8371
  if (mounted.has(host.unit)) continue;
6886
8372
  const already = mountFailures.get(host.unit);
@@ -6920,6 +8406,23 @@ function apply(ctx) {
6920
8406
  });
6921
8407
  }
6922
8408
  sync();
8409
+ if (pluginTools !== void 0) {
8410
+ const health = routes.health;
8411
+ routes.health = async (search) => ({
8412
+ ...await health?.(search),
8413
+ pluginTools: pluginTools.report()
8414
+ });
8415
+ ctx.effect(() => {
8416
+ pluginTools.refresh().catch((error) => {
8417
+ warn(ctx, `探 opencli 插件的命令面时抛了错:${String(error)}`);
8418
+ });
8419
+ return () => pluginTools.dispose();
8420
+ }, `${PLUGIN_ID}: 已装且上板的 opencli 插件各合成一个单元(工具面照旧由面板那道闸收窄)`);
8421
+ ctx.effect(() => {
8422
+ const off = ctx.get(PANELS_SERVICE)?.subscribe?.(() => pluginTools.touch());
8423
+ return () => off?.();
8424
+ }, `${PLUGIN_ID}: 面板配置一动就重探插件命令面(装上 / 收起都在那一条链上)`);
8425
+ }
6923
8426
  ctx.effect(() => () => {
6924
8427
  for (const id of [...mounted.keys()]) unmountUnit(id);
6925
8428
  for (const dispose of undoPost) dispose?.();
@@ -6949,11 +8452,15 @@ function apply(ctx) {
6949
8452
  return () => off?.();
6950
8453
  }, `${PLUGIN_ID}: 事实归属(读工具与写工具同一处声明)`);
6951
8454
  ctx.effect(() => {
6952
- const off = ctx.on?.("agent/created", (payload) => toolScope.apply(payload?.agent));
8455
+ const off = ctx.on?.("agent/created", (payload) => {
8456
+ toolScope.apply(payload?.agent);
8457
+ pluginTools?.touch();
8458
+ });
6953
8459
  return () => off?.();
6954
8460
  }, `${PLUGIN_ID}: agent 的工具面按工作区收窄(创建时)`);
6955
8461
  ctx.effect(() => {
6956
8462
  const off = ctx.on?.("system-prompt/assemble", async (_assembly, context, next) => {
8463
+ pluginTools?.touch();
6957
8464
  const resolved = await next();
6958
8465
  return toolScope.narrow(resolved, context?.scope);
6959
8466
  });
@@ -7006,6 +8513,7 @@ function apply(ctx) {
7006
8513
  return () => off?.();
7007
8514
  }, `${PLUGIN_ID}: 对话引用注入`);
7008
8515
  ctx.effect(() => registerFactStream(ctx, facts), `${PLUGIN_ID}: 变更流`);
8516
+ ctx.effect(() => registerPanelState(ctx, panelState), `${PLUGIN_ID}: 面板运行态上报(浏览器半侧报自己的取数与渲染收尾)`);
7009
8517
  ctx.effect(() => registerApi(ctx, routes), `${PLUGIN_ID}: HTTP 投影`);
7010
8518
  }
7011
8519
  //#endregion