@dsh-workbench/core 0.2.0 → 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 (55) hide show
  1. package/client.js +1427 -607
  2. package/dist/client/client.body.js +1427 -607
  3. package/dist/client/index.js +1428 -608
  4. package/dist/host/index.js +645 -154
  5. package/dist/types/app/client.d.ts +165 -81
  6. package/dist/types/app/layouts/__tests__/async-feedback.test.d.ts +1 -0
  7. package/dist/types/app/layouts/market/use-add-module.d.ts +1 -0
  8. package/dist/types/app/layouts/rightbar/MarketView.d.ts +3 -1
  9. package/dist/types/app/layouts/rightbar/{PanelPluginRows.d.ts → PanelGroupRows.d.ts} +3 -2
  10. package/dist/types/app/layouts/rightbar/PanelRowControls.d.ts +0 -1
  11. package/dist/types/app/layouts/rightbar/__tests__/market-catalog.test.d.ts +1 -0
  12. package/dist/types/app/layouts/rightbar/__tests__/market-dots.test.d.ts +1 -0
  13. package/dist/types/app/layouts/rightbar/__tests__/market-state-rules.test.d.ts +1 -0
  14. package/dist/types/app/layouts/rightbar/__tests__/panel-dismiss.test.d.ts +1 -0
  15. package/dist/types/app/layouts/rightbar/__tests__/panel-drag-target.test.d.ts +1 -0
  16. package/dist/types/app/layouts/rightbar/__tests__/panel-write.test.d.ts +1 -0
  17. package/dist/types/app/layouts/rightbar/board-selection.d.ts +9 -0
  18. package/dist/types/app/layouts/rightbar/dsh-borrow.d.ts +15 -0
  19. package/dist/types/app/layouts/rightbar/dsh-borrow.test.d.ts +1 -0
  20. package/dist/types/app/layouts/rightbar/dsh-surface.d.ts +11 -0
  21. package/dist/types/app/layouts/rightbar/dsh-surface.test.d.ts +1 -0
  22. package/dist/types/app/layouts/rightbar/market-installed-mock.d.ts +9 -0
  23. package/dist/types/app/layouts/rightbar/market-mock-types.d.ts +58 -0
  24. package/dist/types/app/layouts/rightbar/market-mock.d.ts +2 -48
  25. package/dist/types/app/layouts/rightbar/market-row-types.d.ts +4 -2
  26. package/dist/types/app/layouts/rightbar/market-rows.d.ts +9 -22
  27. package/dist/types/app/layouts/rightbar/market-section-icon.d.ts +1 -1
  28. package/dist/types/app/layouts/rightbar/market-shared-rows.d.ts +21 -0
  29. package/dist/types/app/layouts/rightbar/panel-dismiss.d.ts +2 -0
  30. package/dist/types/app/layouts/rightbar/panel-mock.d.ts +1 -1
  31. package/dist/types/app/layouts/rightbar/panel-write.d.ts +15 -0
  32. package/dist/types/app/layouts/rightbar/use-drag-listeners.d.ts +6 -0
  33. package/dist/types/app/layouts/sidebar/__tests__/session-row-ui.test.d.ts +1 -0
  34. package/dist/types/app/units/registry.d.ts +4 -1
  35. package/dist/types/modules/panels/__tests__/diy-harness.d.ts +19 -0
  36. package/dist/types/modules/panels/__tests__/diy-report.test.d.ts +1 -0
  37. package/dist/types/modules/panels/__tests__/diy-tools-health.test.d.ts +1 -0
  38. package/dist/types/modules/panels/__tests__/diy-tools-panel.test.d.ts +1 -0
  39. package/dist/types/modules/panels/model/config.d.ts +4 -3
  40. package/dist/types/modules/panels/model/diy-report.d.ts +63 -0
  41. package/dist/types/shared/api/host/__tests__/approval-note-injection.test.d.ts +1 -0
  42. package/dist/types/shared/api/host/__tests__/panel-state-route.test.d.ts +1 -0
  43. package/dist/types/shared/api/host/installed-module.d.ts +2 -0
  44. package/dist/types/shared/api/host/mount-units.d.ts +1 -0
  45. package/dist/types/shared/api/host/unit-registry.d.ts +6 -0
  46. package/dist/types/shared/api/unit-manifests.d.ts +1 -0
  47. package/dist/types/shared/components/PanelFoot.d.ts +1 -1
  48. package/dist/types/shared/hooks/panel-state-report.d.ts +13 -0
  49. package/dist/types/shared/hooks/panel-state-report.test.d.ts +1 -0
  50. package/docs/HANDOFF.md +7 -0
  51. package/package.json +2 -2
  52. package/preset/diy.patch.yml +5 -26
  53. package/skills/workbench-module-diy/REVIEW.md +22 -0
  54. package/skills/workbench-module-diy/SKILL.md +3 -1
  55. 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,202 @@ 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
+ }
215
418
  //#endregion
216
419
  //#region ../shared/dist/host/cli-error.js
217
420
  function unquote(raw) {
@@ -1403,6 +1606,42 @@ function registerActions(ctx, resources, wrote) {
1403
1606
  ...wrote === void 0 ? {} : { wrote }
1404
1607
  });
1405
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
+ }
1406
1645
  //#endregion
1407
1646
  //#region ../shared/dist/host/long-task.js
1408
1647
  var LIVE = /* @__PURE__ */ new Set(["running", "needs-login"]);
@@ -1796,15 +2035,48 @@ function resolvePluginSource(source, env, options = {}) {
1796
2035
  }
1797
2036
  //#endregion
1798
2037
  //#region ../shared/dist/host/tool-approval.js
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
+ }
1799
2067
  function messageOf$1(error) {
1800
2068
  return error instanceof Error ? error.message : String(error);
1801
2069
  }
1802
- function compose(detail, verdict) {
1803
- if (detail === void 0 || typeof detail.zh !== "string" || detail.zh.trim() === "") return verdict.fallback;
1804
- return {
2070
+ function compose(detail, verdict, note) {
2071
+ const base = detail === void 0 || typeof detail.zh !== "string" || detail.zh.trim() === "" ? verdict.fallback : {
1805
2072
  en: `${detail.en} — ${verdict.question.en}`,
1806
2073
  zh: `${detail.zh} —— ${verdict.question.zh}`
1807
2074
  };
2075
+ if (note === void 0) return base;
2076
+ return {
2077
+ en: `${base.en} — ${note}`,
2078
+ zh: `${base.zh} —— ${note}`
2079
+ };
1808
2080
  }
1809
2081
  async function approvalFor(call, context = {}) {
1810
2082
  if (context.writes !== true) return void 0;
@@ -1838,7 +2110,7 @@ async function approvalFor(call, context = {}) {
1838
2110
  return {
1839
2111
  kind: "ask",
1840
2112
  reason: verdict.reason,
1841
- displayReason: compose(facts, verdict)
2113
+ displayReason: compose(facts, verdict, approvalNoteOf(call))
1842
2114
  };
1843
2115
  }
1844
2116
  //#endregion
@@ -2207,6 +2479,22 @@ function profileModules(profileDir) {
2207
2479
  }
2208
2480
  return found;
2209
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
+ }
2210
2498
  function registryFromLockfile(profileDir, pkg) {
2211
2499
  let text;
2212
2500
  try {
@@ -2667,40 +2955,58 @@ function strings(raw) {
2667
2955
  function parseBoard(raw) {
2668
2956
  if (typeof raw !== "object" || raw === null) return EMPTY_BOARD;
2669
2957
  const value = raw;
2958
+ const order = strings(value.order);
2670
2959
  return {
2671
2960
  shown: strings(value.shown),
2672
- hidden: strings(value.hidden)
2961
+ hidden: strings(value.hidden),
2962
+ ...order.length === 0 ? {} : { order }
2673
2963
  };
2674
2964
  }
2675
2965
  function parseReleased(raw) {
2676
2966
  if (typeof raw !== "object" || raw === null) return [];
2677
2967
  return strings(raw.ids);
2678
2968
  }
2679
- function resolvePanels(manifests, boards) {
2680
- const ordered = [.../* @__PURE__ */ new Set([...boards.workspace?.shown ?? [], ...boards.machine?.shown ?? []])];
2681
- const rank = new Map(ordered.map((id, index) => [id, index]));
2682
- 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;
2683
2986
  const on = resolveScope({
2684
2987
  id: manifest.id,
2685
2988
  boards
2686
2989
  });
2687
- 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;
2688
2991
  return {
2689
2992
  id: manifest.id,
2690
2993
  titleKey: manifest.card?.titleKey ?? manifest.id,
2691
2994
  order,
2692
2995
  shown: on,
2693
2996
  source: overrideOf(boards.workspace, manifest.id) === void 0 ? "default" : "user",
2694
- hasCard: true
2997
+ hasCard
2695
2998
  };
2696
- }).sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
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
+ });
2697
3003
  }
2698
3004
  var PLUGIN_PANEL_PREFIX = "plugin:";
2699
3005
  function pluginPanelId(pkg) {
2700
3006
  return `${PLUGIN_PANEL_PREFIX}${pkg}`;
2701
3007
  }
2702
3008
  function resolvePluginPanels(plugins, boards) {
2703
- const ordered = [.../* @__PURE__ */ new Set([...boards.workspace?.shown ?? [], ...boards.machine?.shown ?? []])];
3009
+ const ordered = [...new Set(boards.workspace?.order ?? [])];
2704
3010
  const rank = new Map(ordered.map((id, index) => [id, index]));
2705
3011
  const seen = /* @__PURE__ */ new Set();
2706
3012
  return plugins.map((plugin) => {
@@ -2735,16 +3041,22 @@ function added(list, id) {
2735
3041
  function dropped(list, id) {
2736
3042
  return list.filter((item) => item !== id);
2737
3043
  }
2738
- function withShown(board, id) {
2739
- return {
3044
+ function withShown(board, id, visible) {
3045
+ const next = {
2740
3046
  shown: added(board.shown, id),
2741
3047
  hidden: dropped(board.hidden, id)
2742
3048
  };
3049
+ return visible === void 0 ? next : {
3050
+ ...next,
3051
+ order: added(board.order ?? visible, id)
3052
+ };
2743
3053
  }
2744
3054
  function withHidden(board, id) {
3055
+ const order = dropped(board.order ?? [], id);
2745
3056
  return {
2746
3057
  shown: dropped(board.shown, id),
2747
- hidden: added(board.hidden, id)
3058
+ hidden: added(board.hidden, id),
3059
+ ...order.length === 0 ? {} : { order }
2748
3060
  };
2749
3061
  }
2750
3062
  function withoutPanel(board, id) {
@@ -2753,71 +3065,15 @@ function withoutPanel(board, id) {
2753
3065
  hidden: dropped(board.hidden, id)
2754
3066
  };
2755
3067
  }
2756
- function withMoved(board, id, index) {
2757
- const current = [...board.shown];
3068
+ function withMoved(board, id, index, visible) {
3069
+ const current = visible.includes(id) ? [...visible] : [...visible, id];
2758
3070
  const from = current.indexOf(id);
2759
- const max = from === -1 ? current.length : current.length - 1;
2760
- const to = Math.max(0, Math.min(max, Math.trunc(index)));
2761
- if (from === -1) current.splice(to, 0, id);
2762
- else if (from !== to) current.splice(to, 0, ...current.splice(from, 1));
2763
- return {
2764
- shown: current,
2765
- hidden: dropped(board.hidden, id)
2766
- };
2767
- }
2768
- //#endregion
2769
- //#region src/shared/api/host/diy-facts.ts
2770
- function asExported(dir) {
2771
- try {
2772
- const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
2773
- return (typeof manifest.exports === "object" && manifest.exports !== null ? Object.values(manifest.exports) : []).filter((one) => typeof one === "string");
2774
- } catch {
2775
- return [];
2776
- }
2777
- }
2778
- function mtimeOf(file) {
2779
- try {
2780
- return statSync(file).mtimeMs;
2781
- } catch {
2782
- return;
2783
- }
2784
- }
2785
- function newestMtime(dir) {
2786
- let newest;
2787
- const walk = (at) => {
2788
- let entries;
2789
- try {
2790
- entries = readdirSync(at, { withFileTypes: true });
2791
- } catch {
2792
- return;
2793
- }
2794
- for (const entry of entries) {
2795
- const full = join(at, entry.name);
2796
- if (entry.isDirectory()) walk(full);
2797
- else if (entry.isFile()) {
2798
- const stamp = mtimeOf(full);
2799
- if (stamp !== void 0 && (newest === void 0 || stamp > newest)) newest = stamp;
2800
- }
2801
- }
2802
- };
2803
- walk(dir);
2804
- return newest;
2805
- }
2806
- function moduleBuildFacts(dir) {
2807
- const artifacts = asExported(dir).map((rel) => ({
2808
- path: rel,
2809
- mtime: mtimeOf(join(dir, rel))
2810
- }));
2811
- const present = artifacts.filter((one) => one.mtime !== void 0);
2812
- const oldestBuilt = present.reduce((min, one) => min === void 0 || one.mtime < min ? one.mtime : min, void 0);
2813
- const newestSource = newestMtime(join(dir, "src"));
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));
2814
3073
  return {
2815
- artifacts: artifacts.map((one) => ({
2816
- path: one.path,
2817
- at: one.mtime === void 0 ? null : new Date(one.mtime).toISOString()
2818
- })),
2819
- builtAll: present.length === artifacts.length && artifacts.length > 0,
2820
- stale: newestSource !== void 0 && oldestBuilt !== void 0 && newestSource > oldestBuilt
3074
+ shown: added(board.shown, id),
3075
+ hidden: dropped(board.hidden, id),
3076
+ order: current
2821
3077
  };
2822
3078
  }
2823
3079
  //#endregion
@@ -3040,10 +3296,169 @@ function installedPluginsOf(dir = vendorPluginsDir()) {
3040
3296
  }));
3041
3297
  }
3042
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
3043
3457
  //#region src/modules/panels/model/diy-tools.ts
3044
3458
  var ACTIONS$2 = [
3045
3459
  "list",
3046
3460
  "status",
3461
+ "panel",
3047
3462
  "probe",
3048
3463
  "recon",
3049
3464
  "install",
@@ -3099,33 +3514,6 @@ function verdictOf$1(change) {
3099
3514
  const tail = (change.packageResult?.output ?? "").trim().split("\n").slice(-3).join(" | ");
3100
3515
  return `没装上(${change.errorCode ?? "failed"})${tail === "" ? "" : `:${tail}`}`;
3101
3516
  }
3102
- function statusOf(options) {
3103
- const { dir, unit, name, installed, known, open } = options;
3104
- const facts = moduleBuildFacts(dir);
3105
- 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 刷新页面),不要再装一遍";
3106
- return {
3107
- unit,
3108
- name,
3109
- dir,
3110
- installed,
3111
- known,
3112
- open,
3113
- artifacts: facts.artifacts,
3114
- stale: facts.stale,
3115
- next
3116
- };
3117
- }
3118
- function renderStatus(packagesDir, lines) {
3119
- if (lines.length === 0) return renderList(packagesDir, []);
3120
- return `工坊:${packagesDir}\n${lines.map((one) => {
3121
- return [
3122
- `—— ${one.unit}(${one.name})`,
3123
- ` 装:${one.installed ? "是" : "没有"} · 面板认得:${one.known ? "是" : "不是"} · 这个项目上开着:${one.open ? "是" : "没有"}`,
3124
- one.artifacts.length === 0 ? " 产物:没有" : ` 产物:${one.artifacts.map((item) => `${item.path}${item.at === null ? "(缺)" : ` ${item.at.slice(11, 19)}Z`}`).join(" · ")} · 源码比产物新:${one.stale ? "是" : "没有"}`,
3125
- ` 下一步:${one.next}`
3126
- ].join("\n");
3127
- }).join("\n")}`;
3128
- }
3129
3517
  function renderProbe(out) {
3130
3518
  if (out.ok) return `读到了 —— 这就是那条读的返回值,把它固化成模块宿主半侧的那一条:\n${(JSON.stringify(out.value, null, 2) ?? "undefined").slice(0, 2e3)}`;
3131
3519
  const lines = [`没读到(${out.code}):${out.message}`];
@@ -3146,12 +3534,6 @@ function renderFind(out) {
3146
3534
  const more = out.entries.length > 20 ? `\n… 还有 ${String(out.entries.length - 20)} 个` : "";
3147
3535
  return `找到了 ${String(out.count)} 个元素 —— 每行的单元格按换行拼在 text 里;把它固化成模块宿主半侧的那一条(host 的 browserFind + 切格 + 按列序取值):\n${head.join("\n")}${more}`;
3148
3536
  }
3149
- function renderList(packagesDir, modules) {
3150
- if (modules.length === 0) return `工坊里还没有模块(${packagesDir})—— 在里面造一个:pnpm exec dsh-wb-module init <名字> --dir <工坊>/packages/module-<名字> --template diy`;
3151
- const lines = modules.map((one) => `${one.unit} · ${one.installed ? "已装" : "没装"} · ${one.name}` + (one.description === "" ? "" : `\n${" ".repeat(one.unit.length)} ${one.description}`));
3152
- const footer = modules.filter((one) => !one.installed).length === 0 ? "都已装过 —— **不要再装**:改源码 + dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面)。" : "装:workbench_diy(action=install, unit=<上面那个 unit>)";
3153
- return `工坊:${packagesDir}\n${lines.join("\n")}\n\n${footer}`;
3154
- }
3155
3537
  function showOrReason(panels, unit, workspace) {
3156
3538
  try {
3157
3539
  panels.show(unit, workspace);
@@ -3171,18 +3553,18 @@ function registerDiyTools(ctx, market, panels) {
3171
3553
  }
3172
3554
  const tool = {
3173
3555
  name: "workbench_diy",
3174
- description: "看 DIY 工坊里的模块现在走到哪一步,或者把它们装上 / 上板。只认工坊里的模块(不是市场 —— 装别的东西用 workbench_modules)。status 会把五件事一次说清(装了没 / 面板认得没 / 这个项目上开着没 / 产物齐不齐 / 源码是否比产物新)并给出下一步,所以别自己去拼这些状态、也别反复 install。recon 是**先看有没有现成的**:拿一个关键词(站点名 / 系统名 / 域名)去 opencli 自带的那份目录里找,回它有哪些命令、要不要用户的 Chrome 登录;都没有就老实说,并给出\"写 adapter\"或\"探一条读\"两条路。用户不知道 opencli 是什么 —— 这一步别问他,自己查。probe 是**探一条读**:用用户浏览器里现有的登录去读一次,把结果与失败码归一(两拍、重试、平台码映射都在里面),探明之后再把它固化成模块宿主半侧的那一条。两种读法二选一:script(页面里跑一段回 JSON 的脚本,读接口)或 css(给一个选择器,读 DOM 那一行 —— 页面里没有接口时用这个,别写 document.querySelectorAll)。装是这台机器上的持久变更,要用户点头;**一辈子只装一次**:之后改源码只需 dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面),不要再调它。",
3175
- 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({
3176
3558
  type: "object",
3177
3559
  properties: {
3178
3560
  action: {
3179
3561
  type: "string",
3180
- description: "list=工坊里有哪些模块(默认);status=某个模块现在走到哪一步、下一步做什么;probe=在用户浏览器里探一条读;recon=先查 opencli 现成的能力(自带 adapter / 本机插件);open=只把它在这个项目上板(不装);install=装上并上板(第一次、会让用户点头)。",
3562
+ description: "list=工坊里有哪些模块(默认);status=某个模块现在走到哪一步、下一步做什么;panel=这个工作区的面板上现在是什么样(谁读到了 / 谁读不到 / 谁崩过);probe=在用户浏览器里探一条读;recon=先查 opencli 现成的能力(自带 adapter / 本机插件);open=只把它在这个项目上板(不装);install=装上并上板(第一次、会让用户点头)。",
3181
3563
  enum: [...ACTIONS$2]
3182
3564
  },
3183
3565
  unit: {
3184
3566
  type: "string",
3185
- description: "status / open / install 填:工坊里那个模块的单元 id(list 会给)。"
3567
+ description: "status / panel / open / install 填:工坊里那个模块的单元 id(list 会给)。"
3186
3568
  },
3187
3569
  script: {
3188
3570
  type: "string",
@@ -3223,7 +3605,7 @@ function registerDiyTools(ctx, market, panels) {
3223
3605
  },
3224
3606
  required: ["action"],
3225
3607
  additionalProperties: false
3226
- },
3608
+ }, { required: true }),
3227
3609
  output: {
3228
3610
  schema: OUTPUT_SCHEMA$2,
3229
3611
  render: (_args, value) => {
@@ -3243,7 +3625,10 @@ function registerDiyTools(ctx, market, panels) {
3243
3625
  const session = sessionWorkspace(exec);
3244
3626
  if (!inWorkshopSession(ctx, session)) throw new Error(`这把工具只在 DIY 工坊里用:当前会话不在工坊里(这个会话的工作区 ${session ?? "(不知道)"})—— 普通项目里装模块用 workbench_modules,工坊的入口是左栏那一行「工作台DIY」`);
3245
3627
  const workspace = session;
3246
- 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);
3247
3632
  if (action === "list") return plainJson({
3248
3633
  workshop: packagesDir,
3249
3634
  modules,
@@ -3262,7 +3647,12 @@ function registerDiyTools(ctx, market, panels) {
3262
3647
  name: one.name,
3263
3648
  installed: one.installed,
3264
3649
  known: panelEntries.some((entry) => entry.id === one.unit),
3265
- 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
+ })
3266
3656
  });
3267
3657
  if (action === "status") {
3268
3658
  const wanted = asText$1(args.unit);
@@ -3275,6 +3665,49 @@ function registerDiyTools(ctx, market, panels) {
3275
3665
  text: renderStatus(packagesDir, rows)
3276
3666
  });
3277
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
+ }
3278
3711
  if (action === "recon") {
3279
3712
  const keywords = keywordsOf(asText$1(args.keyword), asText$1(args.url));
3280
3713
  if (keywords.length === 0) throw new Error("recon 要一个关键词(站点名 / 系统名 / 域名都行):先拿它去 opencli 自带的那份目录里找有没有现成的能力,别一上来就自己写 adapter;用户不知道 opencli 是什么,这一步别问他。");
@@ -4206,7 +4639,7 @@ async function retryAfterRegistryFix(deps, manager, pkg, change) {
4206
4639
  function createMarketService(options) {
4207
4640
  const { manifests, manifestsNow, probe, shell, logger, onInstalled, onUninstalled, isReleased, bundles, installed, modules, workbench, registries, ledger, references } = options;
4208
4641
  const command = options.command ?? "opencli";
4209
- const catalog = () => (manifestsNow?.() ?? manifests).filter((manifest) => (manifest.env?.plugins ?? []).length > 0 || (manifest.env?.libs ?? []).length > 0);
4642
+ const catalog = () => manifestsNow?.() ?? manifests;
4210
4643
  function requireEntry(unit) {
4211
4644
  const manifest = catalog().find((item) => item.id === unit);
4212
4645
  if (manifest === void 0) throw new Error(`市场里没有这个模块:${unit}(可安装:${catalog().map((item) => item.id).join(", ") || "(空)"})`);
@@ -4749,8 +5182,8 @@ function registerMarketTools(ctx, market) {
4749
5182
  }
4750
5183
  const tool = {
4751
5184
  name: "workbench_modules",
4752
- description: "看 / 装 / 升 / 卸工作台的面板模块,以及用户自己装的 opencli 插件。install-bundle 按 spec(包名,可带版本;git / tarball / 本地路径也行)把模块包装进 profile —— 装完会连带装它声明的 opencli 插件;其余 action 按模块 id 动它要的外部供给;update-plugin / uninstall-plugin 按包名或插件名动用户装的那份插件;update-workbench 把工作台自己更新到源上的新版(客户端与宿主一般会自己换上;真要重启时结果里会说,重启由用户来,别自己重启)。卸载只动工作台运行时,不影响终端那套;共用依赖只有升级。",
4753
- parameters: {
5185
+ description: "看 / 装 / 升 / 卸工作台的面板模块,以及用户自己装的 opencli 插件。install-bundle 按 spec(包名,可带版本;git / tarball / 本地路径也行)把模块包装进 profile —— 装完会连带装它声明的 opencli 插件;其余 action 按模块 id 动它要的外部供给;update-workbench 把工作台自己更新到源上的新版(客户端与宿主一般会自己换上;真要重启时结果里会说,重启由用户来,别自己重启)。卸载只动工作台运行时,不影响终端那套;共用依赖只有升级。",
5186
+ parameters: withApprovalNote({
4754
5187
  type: "object",
4755
5188
  properties: {
4756
5189
  action: {
@@ -4777,7 +5210,7 @@ function registerMarketTools(ctx, market) {
4777
5210
  },
4778
5211
  required: ["action"],
4779
5212
  additionalProperties: false
4780
- },
5213
+ }, { required: true }),
4781
5214
  output: {
4782
5215
  schema: OUTPUT_SCHEMA$1,
4783
5216
  render: (_args, value) => {
@@ -4988,9 +5421,11 @@ function createPanelsService(options) {
4988
5421
  return readBoard(folder) ?? EMPTY_BOARD;
4989
5422
  }
4990
5423
  function writeBoard(folder, next) {
5424
+ const order = next.order ?? [];
4991
5425
  folder.write(PANELS_FILE, {
4992
5426
  shown: next.shown,
4993
- hidden: next.hidden
5427
+ hidden: next.hidden,
5428
+ ...order.length === 0 ? {} : { order }
4994
5429
  });
4995
5430
  }
4996
5431
  function boardsOf(workspace) {
@@ -5021,6 +5456,9 @@ function createPanelsService(options) {
5021
5456
  if (next.length === 0) machine.remove(RELEASED_FILE);
5022
5457
  else machine.write(RELEASED_FILE, { ids: next });
5023
5458
  }
5459
+ function ownedSet() {
5460
+ return new Set(owned?.() ?? []);
5461
+ }
5024
5462
  function notify() {
5025
5463
  for (const listener of [...listeners]) try {
5026
5464
  listener();
@@ -5031,7 +5469,7 @@ function createPanelsService(options) {
5031
5469
  function withPersisted(folder, next, key) {
5032
5470
  writeBoard(folder, next);
5033
5471
  notify();
5034
- return resolvePanels(manifests(), boardsOf(key));
5472
+ return resolvePanels(manifests(), boardsOf(key), ownedSet());
5035
5473
  }
5036
5474
  function takeBackReleased(id) {
5037
5475
  const current = readReleased();
@@ -5051,16 +5489,19 @@ function createPanelsService(options) {
5051
5489
  function requireSwitchable(id) {
5052
5490
  if (pluginIds().has(id)) return;
5053
5491
  requireKnown(id);
5054
- 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} 没有卡片,不能收起(它是把别的套件放回来的入口)`);
5055
5493
  }
5056
5494
  function requireWorkspace(workspace) {
5057
5495
  const key = workspace?.trim() ?? "";
5058
5496
  if (key === "") throw new Error("这个会话不属于任何工作区:面板按工作区存,先打开或新建一个工作区");
5059
5497
  return key;
5060
5498
  }
5499
+ function visibleOrder(workspace) {
5500
+ return resolvePanels(manifests(), boardsOf(workspace), ownedSet()).filter((entry) => entry.shown && entry.hasCard).map((entry) => entry.id);
5501
+ }
5061
5502
  return {
5062
5503
  list(workspace, target) {
5063
- return resolvePanels(manifests(), target === "machine" ? boardsOf(null) : boardsOf(workspace));
5504
+ return resolvePanels(manifests(), target === "machine" ? boardsOf(null) : boardsOf(workspace), ownedSet());
5064
5505
  },
5065
5506
  listPlugins(workspace, target) {
5066
5507
  return resolvePluginPanels(pluginRefs(), target === "machine" ? boardsOf(null) : boardsOf(workspace));
@@ -5105,7 +5546,7 @@ function createPanelsService(options) {
5105
5546
  requireSwitchable(id);
5106
5547
  const { folder, key } = targetOf(target ?? "workspace", workspace);
5107
5548
  takeBackReleased(id);
5108
- return withPersisted(folder, withShown(boardIn(folder), id), key);
5549
+ return withPersisted(folder, withShown(boardIn(folder), id, key === null ? void 0 : visibleOrder(key)), key);
5109
5550
  },
5110
5551
  hide(id, workspace, target) {
5111
5552
  requireSwitchable(id);
@@ -5114,10 +5555,12 @@ function createPanelsService(options) {
5114
5555
  },
5115
5556
  move(id, index, workspace, target) {
5116
5557
  requireSwitchable(id);
5117
- 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);
5118
5561
  const current = boardIn(folder);
5119
5562
  if (!current.shown.includes(id)) takeBackReleased(id);
5120
- return withPersisted(folder, withMoved(current, id, index), key);
5563
+ return withPersisted(folder, withMoved(current, id, index, visibleOrder(key)), key);
5121
5564
  },
5122
5565
  reset(workspace, target) {
5123
5566
  const { folder, key } = targetOf(target ?? "workspace", workspace);
@@ -5137,7 +5580,7 @@ function createPanelsService(options) {
5137
5580
  if (id === void 0) {
5138
5581
  folder.remove(PANELS_FILE);
5139
5582
  notify();
5140
- return resolvePanels(manifests(), boardsOf(key));
5583
+ return resolvePanels(manifests(), boardsOf(key), ownedSet());
5141
5584
  }
5142
5585
  return withPersisted(folder, withoutPanel(boardIn(folder), id), key);
5143
5586
  }
@@ -5318,13 +5761,13 @@ function registerPanelTools(ctx, panels, values) {
5318
5761
  }
5319
5762
  const tool = {
5320
5763
  name: "workbench_panels",
5321
- description: "看或调右侧面板:启用哪些套件、什么顺序。启用/收起 = 卡片与 agent 工具一起上下线(别的项目不受影响,不删数据)。收起是\"这个工作区明确不要它\",连那些在机器级默认板上开着的套件也照样在这个工作区收起来,之后 show 能放回来。默认只动当前工作区(scope=workspace);scope=machine 动的是\"机器级默认板\" —— 机器级默认板只是还没配过的工作区的默认值,已经自己配过的工作区保持自己的选择,不会被它改。在那份板上 hide = 明确写下\"不要它\",这一笔比名单更管用:名单里还列着它也算不要,没提过它的工作区因此看不到它;show 会把这笔划掉、把它放回去。本工作区那份板只记**改过的那几行**(逐行覆盖):没提到的行跟着机器级默认板走,所以那份板以后改了,没改过的工作区会跟着变。workbench_panels 默认只动当前工作区,动机器级默认板要显式给 scope=machine。",
5322
- parameters: {
5764
+ description: "看或调右侧面板:启用哪些套件、什么顺序。启用/收起 = 卡片与 agent 工具一起上下线(别的项目不受影响,不删数据)。收起是\"这个工作区明确不要它\",连那些在机器级默认板上开着的套件也照样在这个工作区收起来,之后 show 能放回来。顺序只属于工作区:没排过的按各模块自己声明的次序(开着的在前、关着的跟在后面);show 或 move 过就以那次为准。机器级默认板只回答\"默认开哪几行\",没有顺序这一说。scope=machine 动的是\"机器级默认板\" —— 它只是还没配过的工作区的默认值,已经自己配过的工作区保持自己的选择,不会被它改。在那份板上 hide = 明确写下\"不要它\",这一笔比名单更管用:名单里还列着它也算不要,没提过它的工作区因此看不到它;show 会把这笔划掉、把它放回去。本工作区那份板只记**改过的那几行**(逐行覆盖):没提到的行跟着机器级默认板走,所以那份板以后改了,没改过的工作区会跟着变。",
5765
+ parameters: withApprovalNote({
5323
5766
  type: "object",
5324
5767
  properties: {
5325
5768
  action: {
5326
5769
  type: "string",
5327
- description: "show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它(卡片与工具一起下线,默认在机器上也开着的套件同样收起,别的项目不受影响);move=调顺序;reset=清掉这个工作区改过的那些行(改完就全部回到跟着机器级默认板走);follow=把某一行放回跟随(带 id = 只放回那一行;不带 id = 全部放回)。",
5770
+ description: "show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它(卡片与工具一起下线,默认在机器上也开着的套件同样收起,别的项目不受影响);move=调这个工作区的顺序(新开一行时它排在最后);reset=清掉这个工作区改过的那些行(改完就全部回到跟着机器级默认板走);follow=把某一行放回跟随(带 id = 只放回那一行;不带 id = 全部放回)。",
5328
5771
  enum: [
5329
5772
  "list",
5330
5773
  "show",
@@ -5340,17 +5783,17 @@ function registerPanelTools(ctx, panels, values) {
5340
5783
  },
5341
5784
  index: {
5342
5785
  type: "integer",
5343
- description: "move 的位置,0 基(0=最上面)。"
5786
+ description: "move 的位置,0 基(数的是当前开着的那几行)。"
5344
5787
  },
5345
5788
  scope: {
5346
5789
  type: "string",
5347
- description: "workspace(默认)=只动当前工作区;machine=动机器级默认板,那是还没配过的工作区的默认值 —— 已经自己配过的工作区保持自己的选择,不受它影响。",
5790
+ description: "workspace(默认)=只动当前工作区;machine=动机器级默认板,那是还没配过的工作区的默认值 —— 已经自己配过的工作区保持自己的选择,不受它影响。机器级板没有顺序这一说(move 会报错)。",
5348
5791
  enum: ["workspace", "machine"]
5349
5792
  }
5350
5793
  },
5351
5794
  required: ["action"],
5352
5795
  additionalProperties: false
5353
- },
5796
+ }, { required: true }),
5354
5797
  output: {
5355
5798
  schema: OUTPUT_SCHEMA,
5356
5799
  render: (_args, value) => {
@@ -5430,7 +5873,7 @@ function registerPanelTools(ctx, panels, values) {
5430
5873
  var OWNED = [{
5431
5874
  unit: panelsUnit.id,
5432
5875
  manifest: panelsUnit,
5433
- mount({ ctx, logger, manifests, ownedUnits, opencliCommand, probe, store, values }) {
5876
+ mount({ ctx, logger, manifests, ownedUnits, installable, opencliCommand, probe, store, values }) {
5434
5877
  const ledger = createPluginLedger({
5435
5878
  store,
5436
5879
  home: isolatedHome()
@@ -5445,9 +5888,15 @@ var OWNED = [{
5445
5888
  ctx.provide(PANELS_SERVICE, panels);
5446
5889
  const profileDir = profileDirOf(ctx);
5447
5890
  const shell = ctx.get("shell");
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
+ };
5448
5897
  const market = createMarketService({
5449
- manifests: manifests(),
5450
- manifestsNow: manifests,
5898
+ manifests: moduleManifests(),
5899
+ manifestsNow: moduleManifests,
5451
5900
  ...probe === void 0 ? {} : { probe },
5452
5901
  shell,
5453
5902
  ...opencliCommand === void 0 ? {} : { command: opencliCommand },
@@ -6687,6 +7136,28 @@ function withRunsRoute(result, unit) {
6687
7136
  }
6688
7137
  };
6689
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
+ }
6690
7161
  function failureReasonOf(error) {
6691
7162
  if (error instanceof Error) return error.message;
6692
7163
  return typeof error === "string" ? error : String(error);
@@ -6830,6 +7301,9 @@ function mountUnits(units, deps) {
6830
7301
  continue;
6831
7302
  }
6832
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);
6833
7307
  const longTasks = deps.longTasks === void 0 ? void 0 : createLongTaskHost({
6834
7308
  scope: deps.longTasks.scope,
6835
7309
  unit,
@@ -6839,10 +7313,11 @@ function mountUnits(units, deps) {
6839
7313
  try {
6840
7314
  result = host.face.mount({
6841
7315
  manifest: host.manifest,
6842
- ctx: deps.ctx,
7316
+ ctx: mountCtx,
6843
7317
  logger: deps.logger,
6844
7318
  manifests: deps.manifests,
6845
7319
  ownedUnits,
7320
+ ...deps.installable === void 0 ? {} : { installable: deps.installable },
6846
7321
  ...deps.opencliCommand === void 0 ? {} : { opencliCommand: deps.opencliCommand },
6847
7322
  ...deps.probe === void 0 ? {} : { probe: deps.probe },
6848
7323
  ...deps.values === void 0 ? {} : { values: deps.values },
@@ -6907,6 +7382,9 @@ function mountUnits(units, deps) {
6907
7382
  }
6908
7383
  //#endregion
6909
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
+ }
6910
7388
  function sourceLabel(source) {
6911
7389
  if (source.kind === "owned") return "外壳自己";
6912
7390
  if (source.kind === "plugin") return "按已装 opencli 插件合成";
@@ -6916,6 +7394,7 @@ function createUnitRegistry(options) {
6916
7394
  const refusals = [];
6917
7395
  const hosts = [];
6918
7396
  const byUnit = /* @__PURE__ */ new Map();
7397
+ const failures = /* @__PURE__ */ new Map();
6919
7398
  const listeners = /* @__PURE__ */ new Set();
6920
7399
  function notify() {
6921
7400
  for (const listener of listeners) listener();
@@ -6985,7 +7464,14 @@ function createUnitRegistry(options) {
6985
7464
  subscribe(listener) {
6986
7465
  listeners.add(listener);
6987
7466
  return () => listeners.delete(listener);
6988
- }
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)
6989
7475
  };
6990
7476
  }
6991
7477
  //#endregion
@@ -7270,12 +7756,13 @@ function pluginToolParameters(command) {
7270
7756
  const properties = {};
7271
7757
  for (const arg of command.args) properties[arg.name] = propertyOf(arg);
7272
7758
  const required = command.args.filter((arg) => arg.required).map((arg) => arg.name);
7273
- return {
7759
+ const schema = {
7274
7760
  type: "object",
7275
7761
  properties,
7276
7762
  ...required.length === 0 ? {} : { required },
7277
7763
  additionalProperties: false
7278
7764
  };
7765
+ return pluginToolWrites(command) ? withApprovalNote(schema, { required: true }) : schema;
7279
7766
  }
7280
7767
  function pluginToolDescription(command) {
7281
7768
  const described = command.description.trim();
@@ -7675,6 +8162,7 @@ function seedWorkbenchRuntime(ctx) {
7675
8162
  function apply(ctx) {
7676
8163
  const facts = createFactLog();
7677
8164
  const values = createValueHandoff();
8165
+ const panelState = createPanelStateStore();
7678
8166
  seedWorkbenchRuntime(ctx);
7679
8167
  const opencli = opencliCommandOf(ctx);
7680
8168
  const routes = {
@@ -7687,23 +8175,17 @@ function apply(ctx) {
7687
8175
  };
7688
8176
  const renderers = /* @__PURE__ */ new Map();
7689
8177
  const shell = ctx.get("shell");
7690
- let roster;
7691
8178
  const profileDir = profileDirOf(ctx);
7692
8179
  const units = createUnitRegistry({
7693
- declarationOf: (unit) => {
7694
- if (roster === void 0) roster = new Map(profileModules(profileDir).map((row) => [row.unit, row.pkg]));
7695
- const pkg = roster.get(unit);
7696
- return pkg === void 0 ? void 0 : {
7697
- pkg,
7698
- declaration: moduleDeclarationOf(profileDir, pkg)
7699
- };
7700
- },
8180
+ declarationOf: createDeclarationLookup(profileDir),
7701
8181
  logger: ctx.logger
7702
8182
  });
7703
8183
  ctx.provide(UNITS_SERVICE, units);
8184
+ ctx.provide(PANEL_STATE_SERVICE, panelState);
7704
8185
  routes[UNIT_MANIFESTS_ROUTE] = () => ({
7705
8186
  ok: true,
7706
- manifests: units.manifests()
8187
+ manifests: units.manifests(),
8188
+ installable: installableUnitsOf(units)
7707
8189
  });
7708
8190
  assembleHostUnits(units);
7709
8191
  ctx.inject(["workspaceRegistry"], (scoped) => {
@@ -7777,6 +8259,7 @@ function apply(ctx) {
7777
8259
  if (item === void 0) return;
7778
8260
  mounted.delete(id);
7779
8261
  mountFailures.delete(id);
8262
+ units.recovered(id);
7780
8263
  for (const dispose of item.toolDisposers) dispose?.();
7781
8264
  item.gateOff?.();
7782
8265
  item.plan.disposeLongTasks();
@@ -7795,6 +8278,7 @@ function apply(ctx) {
7795
8278
  logger: ctx.logger,
7796
8279
  manifests: liveManifests,
7797
8280
  ownedUnits: () => units.owned(),
8281
+ installable: () => installableUnitsOf(units),
7798
8282
  store,
7799
8283
  opencliCommand: opencli.command,
7800
8284
  probe: (unit, options) => probeOne(unit, options?.force ?? false),
@@ -7809,6 +8293,7 @@ function apply(ctx) {
7809
8293
  reason: failureReasonOf(error),
7810
8294
  host
7811
8295
  });
8296
+ units.fail(id, failureReasonOf(error));
7812
8297
  ctx.logger?.warn?.(`[dsh-workbench] ${id} 套件起不来(装配期):${failureReasonOf(error)}`);
7813
8298
  return;
7814
8299
  }
@@ -7841,6 +8326,7 @@ function apply(ctx) {
7841
8326
  gateOff: one.gate?.subscribe(() => toolScope.reapply())
7842
8327
  });
7843
8328
  mountFailures.delete(id);
8329
+ units.recovered(id);
7844
8330
  } catch (error) {
7845
8331
  for (const route of added) delete routes[route];
7846
8332
  for (const kind of one.contextRenderers.keys()) renderers.delete(kind);
@@ -7852,6 +8338,7 @@ function apply(ctx) {
7852
8338
  reason: failureReasonOf(error),
7853
8339
  host
7854
8340
  });
8341
+ units.fail(id, failureReasonOf(error));
7855
8342
  ctx.logger?.warn?.(`[dsh-workbench] ${id} 套件起不来(注册期):${failureReasonOf(error)}`);
7856
8343
  }
7857
8344
  }
@@ -7876,7 +8363,10 @@ function apply(ctx) {
7876
8363
  function sync() {
7877
8364
  const present = new Set(units.all().map((one) => one.unit));
7878
8365
  for (const id of [...mounted.keys()]) if (!present.has(id)) unmountUnit(id);
7879
- 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
+ }
7880
8370
  for (const host of units.all()) {
7881
8371
  if (mounted.has(host.unit)) continue;
7882
8372
  const already = mountFailures.get(host.unit);
@@ -8023,6 +8513,7 @@ function apply(ctx) {
8023
8513
  return () => off?.();
8024
8514
  }, `${PLUGIN_ID}: 对话引用注入`);
8025
8515
  ctx.effect(() => registerFactStream(ctx, facts), `${PLUGIN_ID}: 变更流`);
8516
+ ctx.effect(() => registerPanelState(ctx, panelState), `${PLUGIN_ID}: 面板运行态上报(浏览器半侧报自己的取数与渲染收尾)`);
8026
8517
  ctx.effect(() => registerApi(ctx, routes), `${PLUGIN_ID}: HTTP 投影`);
8027
8518
  }
8028
8519
  //#endregion