@dsh-workbench/core 0.1.14 → 0.1.16

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 (48) hide show
  1. package/README.md +19 -0
  2. package/bin/dsh-workbench +21 -1
  3. package/client.js +163 -89
  4. package/cordis.patch.yml +40 -0
  5. package/dist/client/client.body.js +163 -89
  6. package/dist/client/index.js +163 -89
  7. package/dist/host/index.js +1474 -93
  8. package/dist/types/app/__tests__/workbench-styles.test.d.ts +1 -0
  9. package/dist/types/app/client.d.ts +39 -15
  10. package/dist/types/app/layouts/rightbar/board-columns.d.ts +8 -0
  11. package/dist/types/app/layouts/rightbar/board-columns.test.d.ts +1 -0
  12. package/dist/types/app/layouts/rightbar/market-rows.d.ts +2 -0
  13. package/dist/types/app/layouts/workbench-styles.d.ts +2 -0
  14. package/dist/types/modules/panels/__tests__/diy-harness.d.ts +51 -0
  15. package/dist/types/modules/panels/__tests__/diy-tools-status.test.d.ts +1 -0
  16. package/dist/types/modules/panels/__tests__/diy-tools.test.d.ts +1 -0
  17. package/dist/types/modules/panels/model/diy-tools.d.ts +4 -0
  18. package/dist/types/modules/panels/unit.d.ts +1 -0
  19. package/dist/types/shared/api/bundle-install.d.ts +1 -0
  20. package/dist/types/shared/api/host/diy-facts.d.ts +9 -0
  21. package/dist/types/shared/api/host/diy-facts.test.d.ts +1 -0
  22. package/dist/types/shared/api/host/diy-recon-fs.d.ts +3 -0
  23. package/dist/types/shared/api/host/diy-recon-fs.test.d.ts +1 -0
  24. package/dist/types/shared/api/host/diy-recon.d.ts +39 -0
  25. package/dist/types/shared/api/host/diy-recon.test.d.ts +1 -0
  26. package/dist/types/shared/api/host/diy-workshop.d.ts +12 -0
  27. package/dist/types/shared/api/host/diy-workshop.test.d.ts +1 -0
  28. package/dist/types/shared/api/host/installed-module.d.ts +2 -0
  29. package/dist/types/shared/api/host/opencli-command.d.ts +10 -0
  30. package/dist/types/shared/api/host/opencli-command.test.d.ts +1 -0
  31. package/dist/types/shared/api/host/runtime-seed.d.ts +40 -0
  32. package/dist/types/shared/api/host/runtime-seed.test.d.ts +1 -0
  33. package/docs/HANDOFF.md +82 -44
  34. package/docs/OPERATIONS.md +22 -1
  35. package/package.json +5 -5
  36. package/scripts/seed-profile.sh +4 -113
  37. package/scripts/seed-workshop.sh +216 -0
  38. package/skills/workbench-module-diy/BUILD.md +36 -66
  39. package/skills/workbench-module-diy/DATASOURCE.md +80 -12
  40. package/skills/workbench-module-diy/DESIGN.md +27 -2
  41. package/skills/workbench-module-diy/LINKAGE.md +49 -19
  42. package/skills/workbench-module-diy/PANEL.md +78 -4
  43. package/skills/workbench-module-diy/REVIEW.md +6 -5
  44. package/skills/workbench-module-diy/SKILL.md +32 -12
  45. package/skills/workbench-module-diy/STEPS.md +4 -0
  46. package/skills/workbench-module-diy/TOOLING.md +47 -0
  47. package/skills/workbench-module-diy/TOOLS.md +147 -5
  48. package/skills/workbench-module-diy/WIRING.md +88 -0
@@ -1,6 +1,8 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import fs, { readFileSync } from "node:fs";
3
- import path, { join } from "node:path";
2
+ import fs, { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
3
+ import path, { delimiter, dirname, join, resolve, sep } from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
4
6
  //#region ../shared/dist/config/constants.js
5
7
  var PLUGIN_ID = "@dsh-workbench/core";
6
8
  var API_PREFIX = "/workbench/api";
@@ -96,6 +98,516 @@ function workspacesRoot() {
96
98
  return `${workbenchRoot()}/workspaces`;
97
99
  }
98
100
  //#endregion
101
+ //#region ../shared/dist/host/opencli-window.js
102
+ var OPENCLI_WINDOW_ENV = "OPENCLI_WINDOW=background";
103
+ function backgroundPrefix() {
104
+ return [quoteShellArg("env"), quoteShellArg(OPENCLI_WINDOW_ENV)];
105
+ }
106
+ function backgroundLine(command, argv) {
107
+ return [
108
+ ...backgroundPrefix(),
109
+ quoteShellArg(command),
110
+ ...argv.map(quoteShellArg)
111
+ ].join(" ");
112
+ }
113
+ //#endregion
114
+ //#region ../shared/dist/host/shell.js
115
+ var DEFAULT_MAX_BYTES = 8388608;
116
+ async function runShell(shell, command, options = {}) {
117
+ const { workdir, timeoutMs = GIT_TIMEOUT_MS, signal } = options;
118
+ const spec = shell.resolve({
119
+ command,
120
+ ...workdir === void 0 ? {} : { workdir },
121
+ timeoutMs,
122
+ onExpiry: "kill",
123
+ stdoutMaxBytes: DEFAULT_MAX_BYTES,
124
+ ...signal === void 0 ? {} : { signal }
125
+ });
126
+ const result = await (await shell.execute(spec)).result();
127
+ return {
128
+ exitCode: result.exitCode,
129
+ timedOut: result.timedOut === true,
130
+ aborted: result.aborted === true,
131
+ stdout: result.stdout.text,
132
+ stderr: result.stderr.text,
133
+ truncated: result.stdout.truncated || result.stderr.truncated
134
+ };
135
+ }
136
+ var DEFAULT_TIMEOUT_MS$1 = 25e3;
137
+ var DEFAULT_RETRY_MS = 1200;
138
+ var DEFAULT_SETTLE_MS$1 = 900;
139
+ var AUTH_CODES = /* @__PURE__ */ new Set([
140
+ "NOT_LOGGED_IN",
141
+ "NOT_LOGIN",
142
+ "AUTH_FAILED",
143
+ "AUTH_REQUIRED",
144
+ "UNAUTHENTICATED",
145
+ "UNAUTHORIZED",
146
+ "TOKEN_EXPIRED"
147
+ ]);
148
+ var PERMISSION_CODES = /* @__PURE__ */ new Set([
149
+ "FORBIDDEN",
150
+ "PERMISSION_DENIED",
151
+ "NO_PERMISSION",
152
+ "EACCES"
153
+ ]);
154
+ function asRecord$7(value) {
155
+ return typeof value === "object" && value !== null ? value : void 0;
156
+ }
157
+ function textOf$3(value) {
158
+ return typeof value === "string" ? value.trim() : "";
159
+ }
160
+ function codeOf(page) {
161
+ const record = asRecord$7(page);
162
+ return record === void 0 ? "" : textOf$3(record["code"]);
163
+ }
164
+ function messageOf$1(page) {
165
+ const record = asRecord$7(page);
166
+ return record === void 0 ? "" : textOf$3(record["message"]);
167
+ }
168
+ function hintOf$1(page) {
169
+ const record = asRecord$7(page);
170
+ const hint = record === void 0 ? "" : textOf$3(record["hint"]);
171
+ return hint === "" ? void 0 : hint;
172
+ }
173
+ function pageFailed(page) {
174
+ const record = asRecord$7(page);
175
+ return record !== void 0 && record["ok"] === false;
176
+ }
177
+ function blockerFor(code) {
178
+ const normalized = code.trim().toUpperCase().replace(/-/g, "_");
179
+ if (normalized === "") return void 0;
180
+ const matches = (set) => [...set].some((one) => normalized === one || normalized.endsWith(`_${one}`));
181
+ if (matches(PERMISSION_CODES)) return "permission-denied";
182
+ if (matches(AUTH_CODES) || normalized.includes("UNAUTH")) return "auth-required";
183
+ }
184
+ function sleep$1(ms) {
185
+ return new Promise((resolve) => setTimeout(resolve, ms));
186
+ }
187
+ async function shellRun(shell, command, argv, timeoutMs) {
188
+ const result = await runShell(shell, backgroundLine(command, argv), { timeoutMs });
189
+ if (result.timedOut) return {
190
+ kind: "timeout",
191
+ detail: `没在 ${Math.round(timeoutMs / 1e3)} 秒内返回`,
192
+ stderr: result.stderr
193
+ };
194
+ if (result.exitCode !== 0) return {
195
+ kind: "failed",
196
+ detail: tailLines(result.stderr || result.stdout, 4),
197
+ stderr: result.stderr
198
+ };
199
+ return {
200
+ kind: "ok",
201
+ stdout: result.stdout
202
+ };
203
+ }
204
+ async function evalOnce(shell, command, session, script, timeoutMs, retryMs) {
205
+ const run = async () => {
206
+ let attempt;
207
+ try {
208
+ attempt = await shellRun(shell, command, [
209
+ "browser",
210
+ session,
211
+ "eval",
212
+ script,
213
+ "--window",
214
+ "background"
215
+ ], timeoutMs);
216
+ } catch (error) {
217
+ return {
218
+ kind: "failure",
219
+ result: {
220
+ ok: false,
221
+ code: "cli-exec-failed",
222
+ message: "读不了:opencli 起不来",
223
+ hint: "确认这台机器上 opencli 能跑(工作台会自己把它挂好)",
224
+ detail: error instanceof Error ? error.message : String(error)
225
+ }
226
+ };
227
+ }
228
+ if (attempt.kind === "timeout") return {
229
+ kind: "failure",
230
+ result: {
231
+ ok: false,
232
+ code: "cli-timeout",
233
+ message: "这次没读到(等超时了)",
234
+ hint: "稍后重试",
235
+ detail: attempt.detail
236
+ }
237
+ };
238
+ if (attempt.kind === "failed") {
239
+ const normalized = attempt.stderr.trim().toUpperCase().replace(/-/g, "_");
240
+ const forbidden = [...PERMISSION_CODES].some((one) => normalized.includes(one));
241
+ return {
242
+ kind: "failure",
243
+ result: {
244
+ ok: false,
245
+ code: forbidden ? "permission-denied" : "cli-exec-failed",
246
+ message: forbidden ? "没有权限在这台机器上读" : "读不了(命令失败)",
247
+ hint: forbidden ? "换一个有权限的账号,或让用户开权限" : "把下面这段原始输出看一眼再重试",
248
+ detail: attempt.detail
249
+ }
250
+ };
251
+ }
252
+ const value = parseJsonLoose(attempt.stdout);
253
+ if (value === void 0) return {
254
+ kind: "failure",
255
+ result: {
256
+ ok: false,
257
+ code: "cli-exec-failed",
258
+ message: "读到了、但不是 JSON",
259
+ hint: "脚本必须回 JSON",
260
+ detail: tailLines(attempt.stdout, 4)
261
+ }
262
+ };
263
+ return {
264
+ kind: "value",
265
+ value
266
+ };
267
+ };
268
+ const first = await run();
269
+ if (first.kind === "value") return first;
270
+ if (first.result.code !== "cli-exec-failed") return first;
271
+ await sleep$1(retryMs);
272
+ return run();
273
+ }
274
+ async function openOnce$1(shell, command, session, url, timeoutMs) {
275
+ let attempt;
276
+ try {
277
+ attempt = await shellRun(shell, command, [
278
+ "browser",
279
+ session,
280
+ "open",
281
+ url,
282
+ "--window",
283
+ "background"
284
+ ], timeoutMs);
285
+ } catch (error) {
286
+ return {
287
+ ok: false,
288
+ code: "cli-exec-failed",
289
+ message: "打不开那个页面",
290
+ hint: "确认 opencli 能跑起来",
291
+ detail: error instanceof Error ? error.message : String(error),
292
+ helpUrl: url
293
+ };
294
+ }
295
+ if (attempt.kind === "ok") return void 0;
296
+ return {
297
+ ok: false,
298
+ code: attempt.kind === "timeout" ? "cli-timeout" : "cli-exec-failed",
299
+ message: "打不开那个页面",
300
+ hint: "在浏览器里手动打开一次,然后重试",
301
+ detail: attempt.detail,
302
+ helpUrl: url
303
+ };
304
+ }
305
+ async function browserRead(options) {
306
+ const { shell, script, url, loginUrl, open = true } = options;
307
+ const command = options.command ?? "opencli";
308
+ const session = options.session ?? "workbench";
309
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
310
+ const retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
311
+ const settleMs = options.settleMs ?? DEFAULT_SETTLE_MS$1;
312
+ const at = Date.now();
313
+ const first = await evalOnce(shell, command, session, script, timeoutMs, retryMs);
314
+ if (first.kind === "failure") return {
315
+ ...first.result,
316
+ at
317
+ };
318
+ let page = first.value;
319
+ if (codeOf(page) === "not-on-site") {
320
+ if (!open || url === void 0) return {
321
+ ok: false,
322
+ code: "cli-exec-failed",
323
+ at,
324
+ message: "没停在那套系统上",
325
+ hint: "先在那个浏览器里把它打开,再读一次",
326
+ detail: JSON.stringify(page),
327
+ ...url === void 0 ? {} : { helpUrl: url }
328
+ };
329
+ const opened = await openOnce$1(shell, command, session, url, timeoutMs);
330
+ if (opened !== void 0) return {
331
+ ...opened,
332
+ at
333
+ };
334
+ await sleep$1(settleMs);
335
+ const second = await evalOnce(shell, command, session, script, timeoutMs, retryMs);
336
+ if (second.kind === "failure") return {
337
+ ...second.result,
338
+ at
339
+ };
340
+ page = second.value;
341
+ if (codeOf(page) === "not-on-site") return {
342
+ ok: false,
343
+ code: "cli-exec-failed",
344
+ at,
345
+ message: "打开了页面、又读了一遍,仍然不在那套系统上",
346
+ hint: "确认这个地址就是那套系统(或者它跳到了别的域)",
347
+ detail: JSON.stringify(page),
348
+ helpUrl: url
349
+ };
350
+ }
351
+ if (!pageFailed(page)) return {
352
+ ok: true,
353
+ code: "ok",
354
+ at,
355
+ value: page,
356
+ message: "读到了"
357
+ };
358
+ const pageCode = codeOf(page) || "read-failed";
359
+ const message = messageOf$1(page) || `读不到(${pageCode})`;
360
+ const blocker = blockerFor(pageCode);
361
+ if (blocker === "auth-required") return {
362
+ ok: false,
363
+ code: blocker,
364
+ at,
365
+ pageCode,
366
+ message,
367
+ hint: "去登录一次,再重试",
368
+ detail: JSON.stringify(page),
369
+ ...loginUrl === void 0 && url === void 0 ? {} : { helpUrl: loginUrl ?? url }
370
+ };
371
+ if (blocker === "permission-denied") return {
372
+ ok: false,
373
+ code: blocker,
374
+ at,
375
+ pageCode,
376
+ message,
377
+ hint: "这个账号没有权限",
378
+ detail: JSON.stringify(page),
379
+ ...loginUrl === void 0 && url === void 0 ? {} : { helpUrl: loginUrl ?? url }
380
+ };
381
+ return {
382
+ ok: false,
383
+ code: pageCode,
384
+ at,
385
+ pageCode,
386
+ message,
387
+ detail: JSON.stringify(page),
388
+ ...hintOf$1(page) === void 0 ? {} : { hint: hintOf$1(page) }
389
+ };
390
+ }
391
+ //#endregion
392
+ //#region ../shared/dist/host/browser-find.js
393
+ var DEFAULT_TIMEOUT_MS = 25e3;
394
+ var DEFAULT_SETTLE_MS = 2500;
395
+ var URL_TIMEOUT_MS = 8e3;
396
+ var DEFAULT_LIMIT = 50;
397
+ var DEFAULT_TEXT_MAX = 200;
398
+ function asRecord$6(value) {
399
+ return typeof value === "object" && value !== null ? value : void 0;
400
+ }
401
+ function textOf$2(value) {
402
+ return typeof value === "string" ? value.trim() : "";
403
+ }
404
+ function entriesOf(value) {
405
+ const record = asRecord$6(value);
406
+ const raw = record === void 0 ? void 0 : record["entries"];
407
+ if (!Array.isArray(raw)) return [];
408
+ const entries = [];
409
+ for (const one of raw) {
410
+ const item = asRecord$6(one);
411
+ if (item === void 0) continue;
412
+ const nth = typeof item["nth"] === "number" ? item["nth"] : entries.length;
413
+ entries.push({
414
+ text: textOf$2(item["text"]),
415
+ tag: textOf$2(item["tag"]),
416
+ nth
417
+ });
418
+ }
419
+ return entries;
420
+ }
421
+ function reasonOf(stdout, stderr) {
422
+ const parsed = asRecord$6(parseJsonLoose(stdout));
423
+ const error = parsed === void 0 ? void 0 : asRecord$6(parsed["error"]);
424
+ const message = error === void 0 ? "" : textOf$2(error["message"]);
425
+ if (message !== "") return message;
426
+ const lines = (stderr.trim() === "" ? stdout : stderr).split("\n").map((one) => one.trim()).filter((one) => one !== "");
427
+ return (lines[lines.length - 1] ?? "").slice(-200);
428
+ }
429
+ function hintOf(stdout) {
430
+ const parsed = asRecord$6(parseJsonLoose(stdout));
431
+ const error = parsed === void 0 ? void 0 : asRecord$6(parsed["error"]);
432
+ const hint = error === void 0 ? "" : textOf$2(error["hint"]);
433
+ return hint === "" ? void 0 : hint;
434
+ }
435
+ function sameOrigin(left, right) {
436
+ try {
437
+ return new URL(left).origin === new URL(right).origin;
438
+ } catch {
439
+ return false;
440
+ }
441
+ }
442
+ function urlOf(stdout) {
443
+ const bare = (stdout.split("\n").map((one) => one.trim()).filter((one) => one !== "")[0] ?? "").replace(/^url:\s*/i, "").trim().replace(/^["']+|["']+$/g, "");
444
+ if (bare.startsWith("{")) {
445
+ const parsed = asRecord$6(parseJsonLoose(bare));
446
+ const url = parsed === void 0 ? "" : textOf$2(parsed["url"]);
447
+ if (url !== "") return url;
448
+ }
449
+ return bare;
450
+ }
451
+ async function currentUrl(shell, command, session) {
452
+ const line = backgroundLine(command, [
453
+ "browser",
454
+ session,
455
+ "get",
456
+ "url",
457
+ "--window",
458
+ "background"
459
+ ]);
460
+ try {
461
+ const result = await runShell(shell, line, { timeoutMs: URL_TIMEOUT_MS });
462
+ if (result.exitCode !== 0) return "";
463
+ return urlOf(result.stdout);
464
+ } catch {
465
+ return "";
466
+ }
467
+ }
468
+ function sleep(ms) {
469
+ return new Promise((resolve) => setTimeout(resolve, ms));
470
+ }
471
+ async function findOnce(shell, command, session, css, limit, textMax, timeoutMs) {
472
+ const result = await runShell(shell, backgroundLine(command, [
473
+ "browser",
474
+ session,
475
+ "find",
476
+ "--css",
477
+ css,
478
+ "--limit",
479
+ String(limit),
480
+ "--text-max",
481
+ String(textMax),
482
+ "--window",
483
+ "background"
484
+ ]), { timeoutMs });
485
+ if (result.timedOut) return {
486
+ kind: "timeout",
487
+ detail: `没在 ${Math.round(timeoutMs / 1e3)} 秒内返回`
488
+ };
489
+ if (result.exitCode !== 0) return {
490
+ kind: "failed",
491
+ detail: reasonOf(result.stdout, result.stderr),
492
+ ...hintOf(result.stdout) === void 0 ? {} : { hint: hintOf(result.stdout) }
493
+ };
494
+ return {
495
+ kind: "ok",
496
+ entries: entriesOf(parseJsonLoose(result.stdout)),
497
+ stdout: result.stdout
498
+ };
499
+ }
500
+ async function openOnce(shell, command, session, url, timeoutMs) {
501
+ const line = backgroundLine(command, [
502
+ "browser",
503
+ session,
504
+ "open",
505
+ url,
506
+ "--window",
507
+ "background"
508
+ ]);
509
+ let result;
510
+ try {
511
+ result = await runShell(shell, line, { timeoutMs });
512
+ } catch (error) {
513
+ return error instanceof Error ? error.message : String(error);
514
+ }
515
+ if (result.timedOut) return `没在 ${Math.round(timeoutMs / 1e3)} 秒内打开`;
516
+ if (result.exitCode !== 0) {
517
+ const reason = reasonOf(result.stdout, result.stderr);
518
+ return reason === "" ? `opencli 打开失败(退出码 ${String(result.exitCode ?? "?")})` : reason;
519
+ }
520
+ }
521
+ async function browserFind(options) {
522
+ const { shell, css, url, open = true } = options;
523
+ const command = options.command ?? "opencli";
524
+ const session = options.session ?? "workbench";
525
+ const limit = options.limit ?? DEFAULT_LIMIT;
526
+ const textMax = options.textMax ?? DEFAULT_TEXT_MAX;
527
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
528
+ const settleMs = options.settleMs ?? DEFAULT_SETTLE_MS;
529
+ const at = Date.now();
530
+ const find = async () => {
531
+ try {
532
+ return await findOnce(shell, command, session, css, limit, textMax, timeoutMs);
533
+ } catch (error) {
534
+ return {
535
+ kind: "failed",
536
+ detail: error instanceof Error ? error.message : String(error),
537
+ hint: "确认这台机器上 opencli 能跑(工作台会自己把它挂好)"
538
+ };
539
+ }
540
+ };
541
+ const canMove = open && url !== void 0;
542
+ if (canMove) {
543
+ const here = await currentUrl(shell, command, session);
544
+ if (here !== "" && !sameOrigin(here, url)) {
545
+ const opened = await openOnce(shell, command, session, url, timeoutMs);
546
+ if (opened !== void 0) return {
547
+ ok: false,
548
+ code: "cli-exec-failed",
549
+ at,
550
+ entries: [],
551
+ count: 0,
552
+ message: "打不开那个页面",
553
+ hint: "在浏览器里手动打开一次,然后重试",
554
+ detail: opened,
555
+ helpUrl: url
556
+ };
557
+ await sleep(settleMs);
558
+ }
559
+ }
560
+ let outcome = await find();
561
+ if (outcome === void 0) return {
562
+ ok: false,
563
+ code: "cli-exec-failed",
564
+ at,
565
+ entries: [],
566
+ count: 0,
567
+ message: "没找到元素"
568
+ };
569
+ if (outcome.kind === "failed" && canMove) {
570
+ if (await openOnce(shell, command, session, url, timeoutMs) === void 0) {
571
+ await sleep(settleMs);
572
+ const second = await find();
573
+ if (second !== void 0) outcome = second;
574
+ }
575
+ }
576
+ if (outcome.kind === "timeout") return {
577
+ ok: false,
578
+ code: "cli-timeout",
579
+ at,
580
+ entries: [],
581
+ count: 0,
582
+ message: "这次没找到元素(等超时了)",
583
+ hint: "稍后重试",
584
+ detail: outcome.detail,
585
+ ...url === void 0 ? {} : { helpUrl: url }
586
+ };
587
+ if (outcome.kind === "failed") {
588
+ const detail = outcome.detail;
589
+ return {
590
+ ok: false,
591
+ code: "cli-exec-failed",
592
+ at,
593
+ entries: [],
594
+ count: 0,
595
+ message: detail === "" ? "没找到元素(opencli 命令失败)" : `没找到元素:${detail}`,
596
+ ...outcome.hint === void 0 ? {} : { hint: outcome.hint },
597
+ ...detail === "" ? {} : { detail },
598
+ ...url === void 0 ? {} : { helpUrl: url }
599
+ };
600
+ }
601
+ return {
602
+ ok: true,
603
+ code: "ok",
604
+ at,
605
+ entries: outcome.entries,
606
+ count: outcome.entries.length,
607
+ message: outcome.entries.length === 0 ? "页面上没有匹配的元素" : `找到 ${String(outcome.entries.length)} 个元素`
608
+ };
609
+ }
610
+ //#endregion
99
611
  //#region ../shared/dist/host/facts.js
100
612
  function createFactLog(options = {}) {
101
613
  const epoch = options.epoch ?? `e${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
@@ -145,29 +657,6 @@ function wroteNow(result) {
145
657
  return typeof wrote === "boolean" ? wrote : void 0;
146
658
  }
147
659
  //#endregion
148
- //#region ../shared/dist/host/shell.js
149
- var DEFAULT_MAX_BYTES = 8388608;
150
- async function runShell(shell, command, options = {}) {
151
- const { workdir, timeoutMs = GIT_TIMEOUT_MS, signal } = options;
152
- const spec = shell.resolve({
153
- command,
154
- ...workdir === void 0 ? {} : { workdir },
155
- timeoutMs,
156
- onExpiry: "kill",
157
- stdoutMaxBytes: DEFAULT_MAX_BYTES,
158
- ...signal === void 0 ? {} : { signal }
159
- });
160
- const result = await (await shell.execute(spec)).result();
161
- return {
162
- exitCode: result.exitCode,
163
- timedOut: result.timedOut === true,
164
- aborted: result.aborted === true,
165
- stdout: result.stdout.text,
166
- stderr: result.stderr.text,
167
- truncated: result.stdout.truncated || result.stderr.truncated
168
- };
169
- }
170
- //#endregion
171
660
  //#region ../shared/dist/host/provision.js
172
661
  var PROVISION_ACTIONS = [
173
662
  "install",
@@ -197,10 +686,10 @@ var TARGET_CODE = {
197
686
  name: "workbench/no-name",
198
687
  requestId: "workbench/no-request-id"
199
688
  };
200
- function asRecord$3(value) {
689
+ function asRecord$5(value) {
201
690
  return typeof value === "object" && value !== null ? value : {};
202
691
  }
203
- function stringsOf$1(value) {
692
+ function stringsOf$2(value) {
204
693
  if (!Array.isArray(value)) return void 0;
205
694
  const items = value.filter((item) => typeof item === "string" && item.trim() !== "");
206
695
  return items.length === 0 ? void 0 : items;
@@ -228,7 +717,7 @@ function requestOf$1(action, value) {
228
717
  };
229
718
  }
230
719
  function parseProvisionBody(body) {
231
- const raw = asRecord$3(body);
720
+ const raw = asRecord$5(body);
232
721
  const action = raw.action;
233
722
  if (typeof action !== "string" || !PROVISION_ACTIONS.includes(action)) return {
234
723
  ok: false,
@@ -248,7 +737,7 @@ function parseProvisionBody(body) {
248
737
  request: requestOf$1(wanted, value.trim())
249
738
  };
250
739
  const registry = typeof raw.registry === "string" && raw.registry.trim() !== "" ? raw.registry.trim() : void 0;
251
- const approvedBuilds = stringsOf$1(raw.approvedBuilds);
740
+ const approvedBuilds = stringsOf$2(raw.approvedBuilds);
252
741
  const requestId = typeof raw.requestId === "string" && raw.requestId.trim() !== "" ? raw.requestId.trim() : void 0;
253
742
  return {
254
743
  ok: true,
@@ -281,6 +770,7 @@ var WorkbenchDataError = class extends Error {
281
770
  };
282
771
  function opencliCommandLine(command, argv, session = "persistent") {
283
772
  return [
773
+ ...backgroundPrefix(),
284
774
  quoteShellArg(command),
285
775
  ...argv.map(quoteShellArg),
286
776
  ...session === "persistent" ? SITE_SESSION_ARGS : [],
@@ -578,7 +1068,7 @@ function capabilityHandler(probe) {
578
1068
  };
579
1069
  };
580
1070
  }
581
- function healthHandler(ctx) {
1071
+ function healthHandler(ctx, opencli) {
582
1072
  return () => {
583
1073
  const dir = ctx.get("profileContext")?.dir;
584
1074
  return {
@@ -587,7 +1077,8 @@ function healthHandler(ctx) {
587
1077
  profile: dir === void 0 ? null : dir.split("/").pop(),
588
1078
  profileDir: dir ?? null,
589
1079
  pid: process.pid,
590
- now: (/* @__PURE__ */ new Date()).toISOString()
1080
+ now: (/* @__PURE__ */ new Date()).toISOString(),
1081
+ ...opencli === void 0 ? {} : { opencli }
591
1082
  };
592
1083
  };
593
1084
  }
@@ -778,6 +1269,14 @@ function toolchainOpencliDir() {
778
1269
  function vendorPrefix() {
779
1270
  return `${isolatedRoot()}/plugins-npm`;
780
1271
  }
1272
+ function vendorPluginsDir() {
1273
+ return `${vendorPrefix()}/node_modules`;
1274
+ }
1275
+ var OPENCLI_CATALOG_ENV = "DSH_WB_OPENCLI_CATALOG";
1276
+ function cliCatalogPath() {
1277
+ const override = process.env[OPENCLI_CATALOG_ENV]?.trim();
1278
+ return override === void 0 || override === "" ? `${toolchainOpencliDir()}/cli-manifest.json` : override;
1279
+ }
781
1280
  function vendorPeerLink() {
782
1281
  const base = `${vendorPrefix()}/node_modules`;
783
1282
  const [scope] = OPENCLI_NPM_PACKAGE.split("/");
@@ -1118,10 +1617,10 @@ var CANCELLATIONS = [
1118
1617
  "too-late",
1119
1618
  "not-running"
1120
1619
  ];
1121
- function asRecord$2(value) {
1620
+ function asRecord$4(value) {
1122
1621
  return typeof value === "object" && value !== null ? value : void 0;
1123
1622
  }
1124
- function asText$2(value) {
1623
+ function asText$4(value) {
1125
1624
  return typeof value === "string" ? value : "";
1126
1625
  }
1127
1626
  function pick(allowed, value) {
@@ -1135,7 +1634,7 @@ function asTextArray(value) {
1135
1634
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1136
1635
  }
1137
1636
  function asRegistries(raw) {
1138
- const record = asRecord$2(raw);
1637
+ const record = asRecord$4(raw);
1139
1638
  if (record === void 0) return null;
1140
1639
  const registry = asRegistry(record.registry);
1141
1640
  if (registry === void 0) return null;
@@ -1146,35 +1645,35 @@ function asRegistries(raw) {
1146
1645
  };
1147
1646
  }
1148
1647
  function asPackageResult(raw) {
1149
- const record = asRecord$2(raw);
1648
+ const record = asRecord$4(raw);
1150
1649
  if (record === void 0) return void 0;
1151
1650
  const kind = pick(FAILURE_KINDS, record.kind);
1152
1651
  return {
1153
1652
  exitCode: typeof record.exitCode === "number" ? record.exitCode : -1,
1154
- output: asText$2(record.output),
1653
+ output: asText$4(record.output),
1155
1654
  truncated: record.truncated === true,
1156
- logPath: asText$2(record.logPath),
1655
+ logPath: asText$4(record.logPath),
1157
1656
  ...kind === void 0 ? {} : { kind }
1158
1657
  };
1159
1658
  }
1160
1659
  function asChange(raw) {
1161
- const record = asRecord$2(raw);
1660
+ const record = asRecord$4(raw);
1162
1661
  if (record === void 0) return null;
1163
1662
  const application = pick(APPLICATIONS, record.application);
1164
1663
  const stage = pick(STAGES, record.stage);
1165
1664
  if (application === void 0 || stage === void 0) return null;
1166
- const error = asRecord$2(record.error);
1665
+ const error = asRecord$4(record.error);
1167
1666
  const packageResult = asPackageResult(record.packageResult);
1168
1667
  const pendingBuilds = asTextArray(record.pendingBuilds);
1169
1668
  const failedAt = pick(["registry", "spec-host"], record.failedAt);
1170
- const bundle = asText$2(record.bundle);
1171
- const errorCode = error === void 0 ? "" : asText$2(error.code);
1172
- const errorDetail = error === void 0 ? "" : asText$2(error.diagnostic);
1669
+ const bundle = asText$4(record.bundle);
1670
+ const errorCode = error === void 0 ? "" : asText$4(error.code);
1671
+ const errorDetail = error === void 0 ? "" : asText$4(error.diagnostic);
1173
1672
  return {
1174
1673
  application,
1175
1674
  stage,
1176
1675
  changed: record.changed === true,
1177
- target: asText$2(record.target),
1676
+ target: asText$4(record.target),
1178
1677
  ...bundle === "" ? {} : { bundle },
1179
1678
  ...errorCode === "" ? {} : { errorCode },
1180
1679
  ...errorDetail === "" ? {} : { errorDetail },
@@ -1184,16 +1683,16 @@ function asChange(raw) {
1184
1683
  };
1185
1684
  }
1186
1685
  function asInspection(raw) {
1187
- const record = asRecord$2(raw);
1686
+ const record = asRecord$4(raw);
1188
1687
  if (record === void 0) return null;
1189
- const status = asText$2(record.status);
1688
+ const status = asText$4(record.status);
1190
1689
  if (status === "refused") {
1191
1690
  const problem = pick(PROBLEMS, record.problem) ?? "unknown";
1192
1691
  const registries = Array.isArray(record.registries) ? record.registries.map(asRegistry).filter((item) => item !== void 0) : [];
1193
1692
  return {
1194
1693
  status: "refused",
1195
1694
  problem,
1196
- reason: asText$2(record.reason),
1695
+ reason: asText$4(record.reason),
1197
1696
  ...registries.length === 0 ? {} : { registries }
1198
1697
  };
1199
1698
  }
@@ -1201,10 +1700,10 @@ function asInspection(raw) {
1201
1700
  const kind = pick(SPEC_KINDS, record.kind);
1202
1701
  const registry = asRegistry(record.registry);
1203
1702
  if (kind === void 0 || registry === void 0) return null;
1204
- const name = asText$2(record.name);
1205
- const version = asText$2(record.version);
1206
- const description = asText$2(record.description);
1207
- const host = asText$2(record.host);
1703
+ const name = asText$4(record.name);
1704
+ const version = asText$4(record.version);
1705
+ const description = asText$4(record.description);
1706
+ const host = asText$4(record.host);
1208
1707
  return {
1209
1708
  status: "accepted",
1210
1709
  kind,
@@ -1242,17 +1741,17 @@ function readBundleManager(ctx) {
1242
1741
  },
1243
1742
  async cancel(requestId) {
1244
1743
  if (typeof face.cancelInstall !== "function") return null;
1245
- return pick(CANCELLATIONS, (asRecord$2(await face.cancelInstall(requestId)) ?? {}).status) ?? null;
1744
+ return pick(CANCELLATIONS, (asRecord$4(await face.cancelInstall(requestId)) ?? {}).status) ?? null;
1246
1745
  }
1247
1746
  };
1248
1747
  }
1249
1748
  //#endregion
1250
1749
  //#region src/shared/api/host/installed-module.ts
1251
1750
  var PACKAGE_NAME$1 = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
1252
- function asRecord$1(value) {
1751
+ function asRecord$3(value) {
1253
1752
  return typeof value === "object" && value !== null ? value : void 0;
1254
1753
  }
1255
- function asText$1(value) {
1754
+ function asText$3(value) {
1256
1755
  return typeof value === "string" ? value : "";
1257
1756
  }
1258
1757
  function isPackageName(value) {
@@ -1263,7 +1762,7 @@ function profileDirOf(ctx) {
1263
1762
  const dir = typeof profile?.dir === "string" ? profile.dir.trim() : "";
1264
1763
  return dir === "" ? null : dir;
1265
1764
  }
1266
- function stringsOf(value) {
1765
+ function stringsOf$1(value) {
1267
1766
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1268
1767
  }
1269
1768
  function profileModules(profileDir) {
@@ -1274,10 +1773,11 @@ function profileModules(profileDir) {
1274
1773
  } catch {
1275
1774
  return [];
1276
1775
  }
1277
- const record = asRecord$1(manifest);
1776
+ const record = asRecord$3(manifest);
1278
1777
  if (record === void 0) return [];
1279
- const bundles = stringsOf(asRecord$1(asRecord$1(record.dsh)?.profile)?.bundles);
1280
- const names = bundles.length > 0 ? bundles : Object.keys(asRecord$1(record.dependencies) ?? {});
1778
+ const bundles = stringsOf$1(asRecord$3(asRecord$3(record.dsh)?.profile)?.bundles);
1779
+ const dependencies = asRecord$3(record.dependencies) ?? {};
1780
+ const names = bundles.length > 0 ? bundles : Object.keys(dependencies);
1281
1781
  const found = [];
1282
1782
  for (const pkg of names) {
1283
1783
  const read = readInstalledModule(profileDir, pkg);
@@ -1285,7 +1785,8 @@ function profileModules(profileDir) {
1285
1785
  pkg: read.pkg,
1286
1786
  unit: read.unit,
1287
1787
  version: read.version,
1288
- ...read.registry === void 0 ? {} : { registry: read.registry }
1788
+ ...read.registry === void 0 ? {} : { registry: read.registry },
1789
+ local: asText$3(dependencies[read.pkg]).startsWith("link:")
1289
1790
  });
1290
1791
  }
1291
1792
  return found;
@@ -1323,24 +1824,24 @@ function readInstalledModule(profileDir, pkg) {
1323
1824
  pkg
1324
1825
  };
1325
1826
  }
1326
- const record = asRecord$1(manifest);
1827
+ const record = asRecord$3(manifest);
1327
1828
  if (record === void 0) return {
1328
1829
  kind: "unknown",
1329
1830
  pkg
1330
1831
  };
1331
- const declared = asRecord$1(asRecord$1(record.dsh)?.workbench);
1332
- const unit = declared === void 0 ? "" : asText$1(declared.unit);
1832
+ const declared = asRecord$3(asRecord$3(record.dsh)?.workbench);
1833
+ const unit = declared === void 0 ? "" : asText$3(declared.unit);
1333
1834
  if (unit === "") return {
1334
1835
  kind: "not-module",
1335
1836
  pkg
1336
1837
  };
1337
- const declaredRegistry = asText$1(asRecord$1(record.publishConfig)?.registry);
1838
+ const declaredRegistry = asText$3(asRecord$3(record.publishConfig)?.registry);
1338
1839
  const registry = declaredRegistry === "" ? registryFromLockfile(profileDir, pkg) : declaredRegistry;
1339
1840
  return {
1340
1841
  kind: "module",
1341
1842
  pkg,
1342
1843
  unit,
1343
- version: asText$1(record.version),
1844
+ version: asText$3(record.version),
1344
1845
  ...registry === "" ? {} : { registry }
1345
1846
  };
1346
1847
  }
@@ -1456,17 +1957,86 @@ function createRegistryNotebook(dir) {
1456
1957
  }
1457
1958
  };
1458
1959
  }
1960
+ //#endregion
1961
+ //#region src/shared/api/host/diy.ts
1962
+ var WORKSHOP_RELATIVE = "workbench/diy";
1963
+ function workshopPathOf(ctx) {
1964
+ const homePath = ctx.get("dshHomePath");
1965
+ if (typeof homePath !== "function") return null;
1966
+ try {
1967
+ return homePath(WORKSHOP_RELATIVE);
1968
+ } catch {
1969
+ return null;
1970
+ }
1971
+ }
1972
+ //#endregion
1973
+ //#region src/shared/api/host/diy-workshop.ts
1974
+ function asRecord$2(value) {
1975
+ return typeof value === "object" && value !== null ? value : void 0;
1976
+ }
1977
+ function asText$2(value) {
1978
+ return typeof value === "string" ? value.trim() : "";
1979
+ }
1980
+ function workshopPackagesDir(ctx) {
1981
+ const root = workshopPathOf(ctx);
1982
+ return root === null ? null : join(root, "packages");
1983
+ }
1984
+ function insideWorkshop(root, target) {
1985
+ const base = existsSync(root) ? realpathSync(root) : resolve(root);
1986
+ const real = existsSync(target) ? realpathSync(target) : resolve(target);
1987
+ return real === base || real.startsWith(base + sep);
1988
+ }
1989
+ function workshopModules(packagesDir, installed = []) {
1990
+ if (!existsSync(packagesDir)) return [];
1991
+ const already = new Set(installed);
1992
+ return readdirSync(packagesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).flatMap((entry) => {
1993
+ const dir = join(packagesDir, entry.name);
1994
+ let manifest;
1995
+ try {
1996
+ manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
1997
+ } catch {
1998
+ return [];
1999
+ }
2000
+ const record = asRecord$2(manifest);
2001
+ const declared = record === void 0 ? void 0 : asRecord$2(asRecord$2(record.dsh)?.workbench);
2002
+ const unit = declared === void 0 ? "" : asText$2(declared.unit);
2003
+ if (unit === "") return [];
2004
+ return [{
2005
+ unit,
2006
+ name: asText$2(record?.name),
2007
+ description: asText$2(record?.description),
2008
+ dir,
2009
+ installed: already.has(unit)
2010
+ }];
2011
+ }).sort((left, right) => left.unit.localeCompare(right.unit));
2012
+ }
2013
+ function inWorkshopSession(ctx, workspace) {
2014
+ const root = workshopPathOf(ctx);
2015
+ return root !== null && workspace !== null && insideWorkshop(root, workspace);
2016
+ }
2017
+ //#endregion
2018
+ //#region src/modules/panels/unit.ts
2019
+ var PANELS_TOOL = "workbench_panels";
2020
+ var MODULES_TOOL = "workbench_modules";
2021
+ var DIY_TOOL = "workbench_diy";
1459
2022
  var panelsUnit = {
1460
2023
  id: "panels",
1461
2024
  kind: "ui",
1462
2025
  card: null,
1463
- tools: [{
1464
- name: "workbench_panels",
1465
- writes: true
1466
- }, {
1467
- name: "workbench_modules",
1468
- writes: true
1469
- }],
2026
+ tools: [
2027
+ {
2028
+ name: PANELS_TOOL,
2029
+ writes: true
2030
+ },
2031
+ {
2032
+ name: MODULES_TOOL,
2033
+ writes: true
2034
+ },
2035
+ {
2036
+ name: DIY_TOOL,
2037
+ writes: true
2038
+ }
2039
+ ],
1470
2040
  host: {
1471
2041
  inject: [],
1472
2042
  optional: ["tools", "shell"]
@@ -1475,7 +2045,13 @@ var panelsUnit = {
1475
2045
  //#endregion
1476
2046
  //#region src/modules/panels/model/approval.ts
1477
2047
  var READ = { kind: "read" };
1478
- var READ_ONLY_ACTIONS = /* @__PURE__ */ new Set(["list", ""]);
2048
+ var READ_ONLY_ACTIONS = /* @__PURE__ */ new Set([
2049
+ "list",
2050
+ "status",
2051
+ "probe",
2052
+ "recon",
2053
+ ""
2054
+ ]);
1479
2055
  function actionOf$1(call) {
1480
2056
  const raw = call.arguments;
1481
2057
  if (typeof raw !== "object" || raw === null) return {};
@@ -1520,6 +2096,23 @@ var panelsApproval = (call) => {
1520
2096
  zh: `agent 想${verb}模块 ${what}(会改这台机器上的插件)。允许吗?`
1521
2097
  });
1522
2098
  }
2099
+ if (call.name === "workbench_diy") {
2100
+ if (readAction) return READ;
2101
+ if (action === "open") return askOf("workbench_diy open puts an already-installed module on this project panel", {
2102
+ en: `it puts ${what} on this project's panel (nothing new gets installed on this machine). Allow?`,
2103
+ zh: `会把${what}摆到**这个项目**的面板上(这台机器上不装新东西)。允许吗?`
2104
+ }, {
2105
+ en: `The agent wants to show the module ${what} on this project's panel. Allow?`,
2106
+ zh: `agent 想把${what}显示在**这个项目**的面板上。允许吗?`
2107
+ });
2108
+ return askOf(`workbench_diy ${action ?? "(unknown action)"} installs a module built in the workshop and opens it on this project's panel`, {
2109
+ en: `it installs ${what} on this machine and opens it on this project's panel. Allow?`,
2110
+ zh: `会把${what}装到这台机器上,并在**这个项目**的面板上打开。允许吗?`
2111
+ }, {
2112
+ en: `The agent wants to install the module ${what} built in the DIY workshop (it changes what is installed on this machine) and open it here. Allow?`,
2113
+ zh: `agent 想把 DIY 工坊里造好的${what}装上(会改这台机器上装的东西),并在**这个项目**的面板上打开。允许吗?`
2114
+ });
2115
+ }
1523
2116
  if (call.name !== "workbench_panels") return void 0;
1524
2117
  if (readAction) return READ;
1525
2118
  if (action === "hide") return askOf("workbench_panels hide removes a module from the panel", {
@@ -1552,6 +2145,628 @@ var panelsApproval = (call) => {
1552
2145
  });
1553
2146
  };
1554
2147
  //#endregion
2148
+ //#region src/shared/api/host/diy-facts.ts
2149
+ function asExported(dir) {
2150
+ try {
2151
+ const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
2152
+ const built = (typeof manifest.exports === "object" && manifest.exports !== null ? Object.values(manifest.exports) : []).filter((one) => typeof one === "string");
2153
+ if (built.length > 0) return built;
2154
+ return Object.values(manifest.dsh?.workbench?.faces ?? {}).filter((one) => typeof one === "string");
2155
+ } catch {
2156
+ return [];
2157
+ }
2158
+ }
2159
+ function mtimeOf(file) {
2160
+ try {
2161
+ return statSync(file).mtimeMs;
2162
+ } catch {
2163
+ return;
2164
+ }
2165
+ }
2166
+ function newestMtime(dir) {
2167
+ let newest;
2168
+ const walk = (at) => {
2169
+ let entries;
2170
+ try {
2171
+ entries = readdirSync(at, { withFileTypes: true });
2172
+ } catch {
2173
+ return;
2174
+ }
2175
+ for (const entry of entries) {
2176
+ const full = join(at, entry.name);
2177
+ if (entry.isDirectory()) walk(full);
2178
+ else if (entry.isFile()) {
2179
+ const stamp = mtimeOf(full);
2180
+ if (stamp !== void 0 && (newest === void 0 || stamp > newest)) newest = stamp;
2181
+ }
2182
+ }
2183
+ };
2184
+ walk(dir);
2185
+ return newest;
2186
+ }
2187
+ function moduleBuildFacts(dir) {
2188
+ const artifacts = asExported(dir).map((rel) => ({
2189
+ path: rel,
2190
+ mtime: mtimeOf(join(dir, rel))
2191
+ }));
2192
+ const present = artifacts.filter((one) => one.mtime !== void 0);
2193
+ const oldestBuilt = present.reduce((min, one) => min === void 0 || one.mtime < min ? one.mtime : min, void 0);
2194
+ const newestSource = newestMtime(join(dir, "src"));
2195
+ return {
2196
+ artifacts: artifacts.map((one) => ({
2197
+ path: one.path,
2198
+ at: one.mtime === void 0 ? null : new Date(one.mtime).toISOString()
2199
+ })),
2200
+ builtAll: present.length === artifacts.length && artifacts.length > 0,
2201
+ stale: newestSource !== void 0 && oldestBuilt !== void 0 && newestSource > oldestBuilt
2202
+ };
2203
+ }
2204
+ //#endregion
2205
+ //#region src/shared/api/host/opencli-command.ts
2206
+ var OPENCLI_PATH_FALLBACK = "opencli";
2207
+ function isolatedOpencliPathOf(ctx) {
2208
+ const homePath = ctx.get("dshHomePath");
2209
+ if (typeof homePath !== "function") return null;
2210
+ try {
2211
+ return join(homePath("workbench/opencli/bin/opencli"));
2212
+ } catch {
2213
+ return null;
2214
+ }
2215
+ }
2216
+ function opencliCommandOf(ctx) {
2217
+ const isolated = isolatedOpencliPathOf(ctx);
2218
+ const fromEnv = process.env.DSH_WORKBENCH_OPENCLI;
2219
+ if (fromEnv !== void 0 && fromEnv !== "") return {
2220
+ command: fromEnv,
2221
+ source: "env",
2222
+ isolated
2223
+ };
2224
+ if (isolated !== null && existsSync(isolated)) return {
2225
+ command: isolated,
2226
+ source: "isolated",
2227
+ isolated
2228
+ };
2229
+ return {
2230
+ command: OPENCLI_PATH_FALLBACK,
2231
+ source: "path",
2232
+ isolated
2233
+ };
2234
+ }
2235
+ //#endregion
2236
+ //#region src/shared/api/host/diy-recon.ts
2237
+ var SITE_LIMIT = 6;
2238
+ var COMMAND_LIMIT = 12;
2239
+ var SHOWN_LIMIT = 6;
2240
+ var PLUGIN_PREFIX = "opencli-plugin-";
2241
+ function text(value) {
2242
+ return typeof value === "string" ? value.trim() : "";
2243
+ }
2244
+ function stringsOf(value) {
2245
+ return Array.isArray(value) ? value.filter((one) => typeof one === "string") : [];
2246
+ }
2247
+ function manifestCommandsOf(raw) {
2248
+ let parsed;
2249
+ try {
2250
+ parsed = JSON.parse(raw);
2251
+ } catch {
2252
+ return [];
2253
+ }
2254
+ if (!Array.isArray(parsed)) return [];
2255
+ const commands = [];
2256
+ for (const one of parsed) {
2257
+ if (typeof one !== "object" || one === null) continue;
2258
+ const row = one;
2259
+ const declared = text(row["command"]);
2260
+ const site = text(row["site"]) || declared.split("/")[0] || "";
2261
+ const name = text(row["name"]) || declared.split("/")[1] || "";
2262
+ const command = declared === "" ? site === "" || name === "" ? "" : `${site}/${name}` : declared;
2263
+ if (command === "" || site === "") continue;
2264
+ const access = text(row["access"]);
2265
+ commands.push({
2266
+ command,
2267
+ site,
2268
+ name: name === "" ? command.slice(command.indexOf("/") + 1) : name,
2269
+ domain: text(row["domain"]).toLowerCase(),
2270
+ description: text(row["description"]),
2271
+ access: access === "" ? "read" : access,
2272
+ strategy: text(row["strategy"]).toLowerCase(),
2273
+ browser: row["browser"] === true,
2274
+ example: text(row["example"]),
2275
+ columns: stringsOf(row["columns"])
2276
+ });
2277
+ }
2278
+ return commands;
2279
+ }
2280
+ function pluginNamesOf(entries) {
2281
+ return entries.filter((one) => one.startsWith(PLUGIN_PREFIX)).map((one) => one.slice(15)).filter((one) => one !== "").sort();
2282
+ }
2283
+ function pluginCommandsOf(files) {
2284
+ return files.filter((one) => one.endsWith(".js")).map((one) => one.slice(0, -3)).filter((one) => one !== "index" && !one.startsWith("_")).sort();
2285
+ }
2286
+ function hostOf(url) {
2287
+ const trimmed = url.trim();
2288
+ if (trimmed === "") return "";
2289
+ try {
2290
+ return new URL(trimmed).hostname.toLowerCase().replace(/^www\./, "");
2291
+ } catch {
2292
+ return "";
2293
+ }
2294
+ }
2295
+ function keywordsOf(keyword, url) {
2296
+ const words = /* @__PURE__ */ new Set();
2297
+ const trimmed = keyword.trim().toLowerCase();
2298
+ if (trimmed !== "") words.add(trimmed);
2299
+ const host = hostOf(url);
2300
+ if (host !== "") words.add(host);
2301
+ return [...words];
2302
+ }
2303
+ function scoreOf(command, word) {
2304
+ const site = command.site.toLowerCase();
2305
+ const name = command.name.toLowerCase();
2306
+ if (word === "") return 0;
2307
+ if (site === word) return 100;
2308
+ if (site.includes(word)) return 80;
2309
+ if (command.domain !== "" && command.domain.includes(word)) return 70;
2310
+ if (site.length >= 3 && word.includes(site)) return 45;
2311
+ if (name === word) return 65;
2312
+ if (name.includes(word)) return 55;
2313
+ if (command.command.toLowerCase().includes(word)) return 50;
2314
+ if (command.description.toLowerCase().includes(word)) return 30;
2315
+ return 0;
2316
+ }
2317
+ function reconOf(commands, plugins, keywords) {
2318
+ const keyword = keywords[0] ?? "";
2319
+ const best = /* @__PURE__ */ new Map();
2320
+ for (const command of commands) {
2321
+ let score = 0;
2322
+ for (const word of keywords) score = Math.max(score, scoreOf(command, word));
2323
+ if (score === 0) continue;
2324
+ const current = best.get(command.command);
2325
+ if (current === void 0 || current.score < score) best.set(command.command, {
2326
+ command,
2327
+ score
2328
+ });
2329
+ }
2330
+ const grouped = /* @__PURE__ */ new Map();
2331
+ for (const entry of best.values()) {
2332
+ const group = grouped.get(entry.command.site) ?? {
2333
+ best: 0,
2334
+ rows: []
2335
+ };
2336
+ group.best = Math.max(group.best, entry.score);
2337
+ group.rows.push(entry);
2338
+ grouped.set(entry.command.site, group);
2339
+ }
2340
+ const base = {
2341
+ keyword,
2342
+ sites: [...grouped.entries()].sort((left, right) => right[1].best - left[1].best || left[0].localeCompare(right[0])).slice(0, SITE_LIMIT).map(([site, group]) => {
2343
+ const picked = [...group.rows].sort((left, right) => (left.command.access === "write" ? 1 : 0) - (right.command.access === "write" ? 1 : 0) || right.score - left.score || left.command.command.localeCompare(right.command.command)).slice(0, COMMAND_LIMIT).map((one) => one.command);
2344
+ const first = picked[0];
2345
+ return {
2346
+ site,
2347
+ domain: first?.domain ?? "",
2348
+ strategy: first?.strategy ?? "",
2349
+ browser: first?.browser === true,
2350
+ commands: picked
2351
+ };
2352
+ }),
2353
+ plugins: plugins.filter((plugin) => {
2354
+ const name = plugin.name.toLowerCase();
2355
+ return keywords.some((word) => word !== "" && (name.includes(word) || word.includes(name)));
2356
+ }),
2357
+ next: "",
2358
+ text: ""
2359
+ };
2360
+ const next = nextOf(base);
2361
+ const withNext = {
2362
+ ...base,
2363
+ next
2364
+ };
2365
+ return {
2366
+ ...withNext,
2367
+ text: textOf$1(withNext)
2368
+ };
2369
+ }
2370
+ function nextOf(answer) {
2371
+ const site = answer.sites[0];
2372
+ if (site !== void 0) {
2373
+ const pick = site.commands.find((one) => one.access !== "write") ?? site.commands[0];
2374
+ return `直接复用:${pick === void 0 ? `opencli ${site.site}` : pick.example === "" ? `opencli ${pick.command}` : pick.example}(这台机器上就能跑,不用装、不用点头)`;
2375
+ }
2376
+ const plugin = answer.plugins[0];
2377
+ if (plugin !== void 0) return `这台机器另装的插件 ${plugin.name} 里有匹配:opencli ${plugin.name} --help 看全部命令`;
2378
+ return "这台机器上没有现成的 —— 两条路:① 给这个站点写 adapter:opencli skills read opencli-adapter-author;② 只读一次:workbench_diy action=probe(script 读接口 / css 读 DOM)";
2379
+ }
2380
+ function textOf$1(answer) {
2381
+ const lines = [];
2382
+ if (answer.sites.length > 0) {
2383
+ lines.push(`opencli 自带这 ${String(answer.sites.length)} 组能力(不用装、不用点头):`);
2384
+ for (const site of answer.sites) {
2385
+ const where = site.domain === "" ? "" : `(${site.domain})`;
2386
+ const chrome = site.browser ? "要用你的 Chrome 登录" : "不用浏览器";
2387
+ lines.push(`· ${site.site}${where} · ${site.strategy === "" ? "—" : site.strategy} · ${chrome}`);
2388
+ for (const command of site.commands.slice(0, SHOWN_LIMIT)) {
2389
+ const what = command.description === "" ? "" : ` —— ${command.description.slice(0, 60)}`;
2390
+ lines.push(` ${command.access === "write" ? "写" : "读"} opencli ${command.command}${what}`);
2391
+ }
2392
+ const rest = site.commands.length - SHOWN_LIMIT;
2393
+ if (rest > 0) lines.push(` … 还有 ${String(rest)} 条:opencli ${site.site} --help`);
2394
+ }
2395
+ }
2396
+ if (answer.plugins.length > 0) lines.push(`这台机器另装的插件里匹配到:${answer.plugins.map((one) => `${one.name}(${String(one.commands.length)} 条命令)`).join("、")}`);
2397
+ if (answer.sites.length === 0 && answer.plugins.length === 0) lines.push(`没匹配到「${answer.keyword}」—— 换英文名或域名再试(例:bilibili、arxiv.org);实在没有就走下面那条。`);
2398
+ lines.push(answer.next);
2399
+ return lines.join("\n");
2400
+ }
2401
+ //#endregion
2402
+ //#region src/shared/api/host/diy-recon-fs.ts
2403
+ function filesOf(path) {
2404
+ try {
2405
+ return readdirSync(path);
2406
+ } catch {
2407
+ return [];
2408
+ }
2409
+ }
2410
+ function reconCatalogOf(path = cliCatalogPath()) {
2411
+ try {
2412
+ return manifestCommandsOf(readFileSync(path, "utf8"));
2413
+ } catch {
2414
+ return;
2415
+ }
2416
+ }
2417
+ function installedPluginsOf(dir = vendorPluginsDir()) {
2418
+ return pluginNamesOf(filesOf(dir)).map((name) => ({
2419
+ name,
2420
+ commands: pluginCommandsOf(filesOf(join(dir, `opencli-plugin-${name}`)))
2421
+ }));
2422
+ }
2423
+ //#endregion
2424
+ //#region src/modules/panels/model/diy-tools.ts
2425
+ var ACTIONS$1 = [
2426
+ "list",
2427
+ "status",
2428
+ "probe",
2429
+ "recon",
2430
+ "install",
2431
+ "open"
2432
+ ];
2433
+ var OUTPUT_SCHEMA$2 = {
2434
+ type: "object",
2435
+ properties: {
2436
+ text: { type: "string" },
2437
+ workshop: { type: "string" },
2438
+ modules: {
2439
+ type: "array",
2440
+ items: {
2441
+ type: "object",
2442
+ additionalProperties: true
2443
+ }
2444
+ },
2445
+ unit: { type: "string" },
2446
+ path: { type: "string" },
2447
+ install: {
2448
+ type: "object",
2449
+ additionalProperties: true
2450
+ },
2451
+ alreadyInstalled: { type: "boolean" },
2452
+ panel: {
2453
+ type: "object",
2454
+ additionalProperties: true
2455
+ }
2456
+ },
2457
+ additionalProperties: true
2458
+ };
2459
+ function asRecord$1(value) {
2460
+ return typeof value === "object" && value !== null ? value : void 0;
2461
+ }
2462
+ function asNumber(value) {
2463
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2464
+ if (typeof value === "string" && value.trim() !== "") {
2465
+ const parsed = Number(value);
2466
+ if (Number.isFinite(parsed)) return parsed;
2467
+ }
2468
+ }
2469
+ function asText$1(value) {
2470
+ return typeof value === "string" ? value.trim() : "";
2471
+ }
2472
+ function plainJson(value) {
2473
+ return JSON.parse(JSON.stringify(value));
2474
+ }
2475
+ function verdictOf(change) {
2476
+ if (change.application === "applied") return "装好了,已生效";
2477
+ if (change.application === "restart-required") return "装好了,要重启一次才生效(装新模块、同名重装、替换已有包都走这一支)";
2478
+ if (change.application === "overridden") return "被更高层的声明盖住了,这次没生效";
2479
+ if (change.application === "cancelled") return "安装被取消,没有生效";
2480
+ const tail = (change.packageResult?.output ?? "").trim().split("\n").slice(-3).join(" | ");
2481
+ return `没装上(${change.errorCode ?? "failed"})${tail === "" ? "" : `:${tail}`}`;
2482
+ }
2483
+ function statusOf(options) {
2484
+ const { dir, unit, name, installed, known, open } = options;
2485
+ const facts = moduleBuildFacts(dir);
2486
+ 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 刷新页面),不要再装一遍";
2487
+ return {
2488
+ unit,
2489
+ name,
2490
+ dir,
2491
+ installed,
2492
+ known,
2493
+ open,
2494
+ artifacts: facts.artifacts,
2495
+ stale: facts.stale,
2496
+ next
2497
+ };
2498
+ }
2499
+ function renderStatus(packagesDir, lines) {
2500
+ if (lines.length === 0) return renderList(packagesDir, []);
2501
+ return `工坊:${packagesDir}\n${lines.map((one) => {
2502
+ return [
2503
+ `—— ${one.unit}(${one.name})`,
2504
+ ` 装:${one.installed ? "是" : "没有"} · 面板认得:${one.known ? "是" : "不是"} · 这个项目上开着:${one.open ? "是" : "没有"}`,
2505
+ one.artifacts.length === 0 ? " 产物:没有" : ` 产物:${one.artifacts.map((item) => `${item.path}${item.at === null ? "(缺)" : ` ${item.at.slice(11, 19)}Z`}`).join(" · ")} · 源码比产物新:${one.stale ? "是" : "没有"}`,
2506
+ ` 下一步:${one.next}`
2507
+ ].join("\n");
2508
+ }).join("\n")}`;
2509
+ }
2510
+ function renderProbe(out) {
2511
+ if (out.ok) return `读到了 —— 这就是那条读的返回值,把它固化成模块宿主半侧的那一条:\n${(JSON.stringify(out.value, null, 2) ?? "undefined").slice(0, 2e3)}`;
2512
+ const lines = [`没读到(${out.code}):${out.message}`];
2513
+ if (out.hint !== void 0) lines.push(`照做:${out.hint}`);
2514
+ if (out.helpUrl !== void 0) lines.push(`入口:${out.helpUrl}`);
2515
+ if (out.pageCode !== void 0) lines.push(`页面自己回的码:${out.pageCode}`);
2516
+ return lines.join("\n");
2517
+ }
2518
+ function renderFind(out) {
2519
+ if (!out.ok) {
2520
+ const lines = [`没找到(${out.code}):${out.message}`];
2521
+ if (out.hint !== void 0) lines.push(`照做:${out.hint}`);
2522
+ if (out.helpUrl !== void 0) lines.push(`入口:${out.helpUrl}`);
2523
+ return lines.join("\n");
2524
+ }
2525
+ if (out.count === 0) return "找到了 0 个元素 —— 选择器没匹配到东西。先确认停在哪一页(probe 会自己问页面),再把选择器放宽一点(例如 table tr 而不是某个 id)。";
2526
+ const head = out.entries.slice(0, 20).map((one, index) => `${String(index + 1)}. ${one.text.replace(/\n+/g, " | ").slice(0, 200)}`);
2527
+ const more = out.entries.length > 20 ? `\n… 还有 ${String(out.entries.length - 20)} 个` : "";
2528
+ return `找到了 ${String(out.count)} 个元素 —— 每行的单元格按换行拼在 text 里;把它固化成模块宿主半侧的那一条(host 的 browserFind + 切格 + 按列序取值):\n${head.join("\n")}${more}`;
2529
+ }
2530
+ function renderList(packagesDir, modules) {
2531
+ if (modules.length === 0) return `工坊里还没有模块(${packagesDir})—— 在里面造一个:pnpm exec dsh-wb-module init <名字> --dir <工坊>/packages/module-<名字> --template diy`;
2532
+ const lines = modules.map((one) => `${one.unit} · ${one.installed ? "已装" : "没装"} · ${one.name}` + (one.description === "" ? "" : `\n${" ".repeat(one.unit.length)} ${one.description}`));
2533
+ const footer = modules.filter((one) => !one.installed).length === 0 ? "都已装过 —— **不要再装**:改源码 + dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面)。" : "装:workbench_diy(action=install, unit=<上面那个 unit>)";
2534
+ return `工坊:${packagesDir}\n${lines.join("\n")}\n\n${footer}`;
2535
+ }
2536
+ function showOrReason(panels, unit, workspace) {
2537
+ try {
2538
+ panels.show(unit, workspace);
2539
+ return { ok: true };
2540
+ } catch (error) {
2541
+ return {
2542
+ ok: false,
2543
+ reason: error instanceof Error ? error.message : String(error)
2544
+ };
2545
+ }
2546
+ }
2547
+ function registerDiyTools(ctx, market, panels) {
2548
+ const tools = ctx.get("tools");
2549
+ if (tools === void 0) {
2550
+ ctx.logger?.info?.("[dsh-workbench] 没有 tools 服务,跳过 DIY 工坊工具注册");
2551
+ return [];
2552
+ }
2553
+ const tool = {
2554
+ name: "workbench_diy",
2555
+ description: "看 DIY 工坊里的模块现在走到哪一步,或者把它们装上 / 上板。只认工坊里的模块(不是市场 —— 装别的东西用 workbench_modules)。status 会把五件事一次说清(装了没 / 面板认得没 / 这个项目上开着没 / 产物齐不齐 / 源码是否比产物新)并给出下一步,所以别自己去拼这些状态、也别反复 install。recon 是**先看有没有现成的**:拿一个关键词(站点名 / 系统名 / 域名)去 opencli 自带的那份目录里找,回它有哪些命令、要不要用户的 Chrome 登录;都没有就老实说,并给出\"写 adapter\"或\"探一条读\"两条路。用户不知道 opencli 是什么 —— 这一步别问他,自己查。probe 是**探一条读**:用用户浏览器里现有的登录去读一次,把结果与失败码归一(两拍、重试、平台码映射都在里面),探明之后再把它固化成模块宿主半侧的那一条。两种读法二选一:script(页面里跑一段回 JSON 的脚本,读接口)或 css(给一个选择器,读 DOM 那一行 —— 页面里没有接口时用这个,别写 document.querySelectorAll)。装是这台机器上的持久变更,要用户点头;**一辈子只装一次**:之后改源码只需 dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面),不要再调它。",
2556
+ parameters: {
2557
+ type: "object",
2558
+ properties: {
2559
+ action: {
2560
+ type: "string",
2561
+ description: "list=工坊里有哪些模块(默认);status=某个模块现在走到哪一步、下一步做什么;probe=在用户浏览器里探一条读;recon=先查 opencli 现成的能力(自带 adapter / 本机插件);open=只把它在这个项目上板(不装);install=装上并上板(第一次、会让用户点头)。",
2562
+ enum: [...ACTIONS$1]
2563
+ },
2564
+ unit: {
2565
+ type: "string",
2566
+ description: "status / open / install 填:工坊里那个模块的单元 id(list 会给)。"
2567
+ },
2568
+ script: {
2569
+ type: "string",
2570
+ description: "probe 要(与 css 二选一):在页面里跑的脚本(必须回 JSON)。不在站点就回 { ok: false, code: \"not-on-site\" };登录过期回 { ok: false, code: \"not-logged-in\" };成功回 { ok: true, ...数据 }。"
2571
+ },
2572
+ css: {
2573
+ type: "string",
2574
+ description: "probe 要(与 script 二选一):一个 CSS 选择器,回页面上匹配到的元素(每行的单元格按换行拼在 text 里)。页面里没有接口时用它 —— 走的是一条固定的 DOM 读(找不到会自己打开 url 再读一遍),别写 document.querySelectorAll。"
2575
+ },
2576
+ limit: {
2577
+ type: "number",
2578
+ description: "probe 配 css 用:最多回几个元素(默认 50)。"
2579
+ },
2580
+ textMax: {
2581
+ type: "number",
2582
+ description: "probe 配 css 用:每个元素文本最多留几个字(默认 200)。"
2583
+ },
2584
+ url: {
2585
+ type: "string",
2586
+ description: "probe / recon 用:那套系统的地址(probe 不在站点上时自己打开它再读一遍;recon 拿它取域名去找现成的 adapter)。"
2587
+ },
2588
+ keyword: {
2589
+ type: "string",
2590
+ description: "recon 要:站点名 / 系统名 / 关键词(英文名或域名最准,例:bilibili、arxiv.org、cmdb)。用户说的是\"我想看 X\",把它翻译成关键词是你的活 —— 别问他用哪个插件。"
2591
+ },
2592
+ loginUrl: {
2593
+ type: "string",
2594
+ description: "probe 用:要登录时那颗\"去登录\"该去哪(默认用 url)。"
2595
+ },
2596
+ session: {
2597
+ type: "string",
2598
+ description: "probe 用:浏览器桥上的会话名(默认 workbench,一般不用填)。"
2599
+ },
2600
+ open: {
2601
+ type: "boolean",
2602
+ description: "probe 用:不在站点上时要不要顺手打开(默认要)。"
2603
+ }
2604
+ },
2605
+ required: ["action"],
2606
+ additionalProperties: false
2607
+ },
2608
+ output: {
2609
+ schema: OUTPUT_SCHEMA$2,
2610
+ render: (_args, value) => {
2611
+ const payload = value;
2612
+ return [{
2613
+ type: "text",
2614
+ text: typeof payload?.text === "string" ? payload.text : JSON.stringify(value)
2615
+ }];
2616
+ }
2617
+ },
2618
+ async execute(raw, exec) {
2619
+ const args = asRecord$1(raw) ?? {};
2620
+ const action = asText$1(args.action) || "list";
2621
+ if (!ACTIONS$1.includes(action)) throw new Error(`不认识的 action:${action}(可用:${ACTIONS$1.join(" / ")})`);
2622
+ const packagesDir = workshopPackagesDir(ctx);
2623
+ if (packagesDir === null) throw new Error("算不出 DIY 工坊在哪(这台机器没给出 dshHomePath)—— 让用户跑一次 dsh-workbench setup");
2624
+ const session = sessionWorkspace(exec);
2625
+ if (!inWorkshopSession(ctx, session)) throw new Error(`这把工具只在 DIY 工坊里用:当前会话不在工坊里(这个会话的工作区 ${session ?? "(不知道)"})—— 普通项目里装模块用 workbench_modules,工坊的入口是左栏那一行「工作台DIY」`);
2626
+ const workspace = session;
2627
+ const modules = workshopModules(packagesDir, profileModules(profileDirOf(ctx)).map((row) => row.unit));
2628
+ if (action === "list") return plainJson({
2629
+ workshop: packagesDir,
2630
+ modules,
2631
+ text: renderList(packagesDir, modules)
2632
+ });
2633
+ const panelEntries = (() => {
2634
+ try {
2635
+ return panels.list(workspace);
2636
+ } catch {
2637
+ return [];
2638
+ }
2639
+ })();
2640
+ const statusOfUnit = (one) => statusOf({
2641
+ dir: one.dir,
2642
+ unit: one.unit,
2643
+ name: one.name,
2644
+ installed: one.installed,
2645
+ known: panelEntries.some((entry) => entry.id === one.unit),
2646
+ open: panels.isEnabled(one.unit, workspace)
2647
+ });
2648
+ if (action === "status") {
2649
+ const wanted = asText$1(args.unit);
2650
+ const picked = wanted === "" ? modules : modules.filter((one) => one.unit === wanted);
2651
+ if (wanted !== "" && picked.length === 0) throw new Error(`工坊里没有单元 id 为「${wanted}」的模块(可用的:${modules.map((one) => one.unit).join(", ") || "(空)"})`);
2652
+ const rows = picked.map(statusOfUnit);
2653
+ return plainJson({
2654
+ workshop: packagesDir,
2655
+ status: rows,
2656
+ text: renderStatus(packagesDir, rows)
2657
+ });
2658
+ }
2659
+ if (action === "recon") {
2660
+ const keywords = keywordsOf(asText$1(args.keyword), asText$1(args.url));
2661
+ if (keywords.length === 0) throw new Error("recon 要一个关键词(站点名 / 系统名 / 域名都行):先拿它去 opencli 自带的那份目录里找有没有现成的能力,别一上来就自己写 adapter;用户不知道 opencli 是什么,这一步别问他。");
2662
+ const catalog = reconCatalogOf();
2663
+ if (catalog === void 0) throw new Error(`读不到 opencli 那份目录(${cliCatalogPath()})—— 隔离运行时还没装好;照做:${isolatedInstallCommand()}`);
2664
+ return plainJson({
2665
+ workshop: packagesDir,
2666
+ ok: true,
2667
+ ...reconOf(catalog, installedPluginsOf(), keywords)
2668
+ });
2669
+ }
2670
+ if (action === "probe") {
2671
+ const script = asText$1(args.script);
2672
+ const css = asText$1(args.css);
2673
+ if (script === "" && css === "") throw new Error("probe 要 script(页面脚本,回 JSON)或 css(读 DOM 那一行的选择器)之一。script 的约定:不在那个站点上就回 { ok: false, code: \"not-on-site\" }(我会自己把它打开再读一遍);登录过期回 { ok: false, code: \"not-logged-in\" };成功回 { ok: true, ...数据 }。页面里压根没有接口时给 css,别写 document.querySelectorAll(宿主产物不许出现 DOM 字样)。");
2674
+ const shell = ctx.get("shell");
2675
+ if (shell === void 0) throw new Error("这台机器上没有 shell 服务,probe 跑不了");
2676
+ const url = asText$1(args.url);
2677
+ const loginUrl = asText$1(args.loginUrl);
2678
+ const session = asText$1(args.session);
2679
+ const open = typeof args.open === "boolean" ? args.open : void 0;
2680
+ if (css !== "") {
2681
+ const limit = asNumber(args.limit);
2682
+ const textMax = asNumber(args.textMax);
2683
+ const found = await browserFind({
2684
+ shell,
2685
+ css,
2686
+ command: opencliCommandOf(ctx).command,
2687
+ ...url === "" ? {} : { url },
2688
+ ...session === "" ? {} : { session },
2689
+ ...open === void 0 ? {} : { open },
2690
+ ...limit === void 0 ? {} : { limit },
2691
+ ...textMax === void 0 ? {} : { textMax }
2692
+ });
2693
+ return plainJson({
2694
+ workshop: packagesDir,
2695
+ ...found,
2696
+ text: renderFind(found)
2697
+ });
2698
+ }
2699
+ const out = await browserRead({
2700
+ shell,
2701
+ script,
2702
+ command: opencliCommandOf(ctx).command,
2703
+ ...url === "" ? {} : { url },
2704
+ ...loginUrl === "" ? {} : { loginUrl },
2705
+ ...session === "" ? {} : { session },
2706
+ ...open === void 0 ? {} : { open }
2707
+ });
2708
+ return plainJson({
2709
+ workshop: packagesDir,
2710
+ ...out,
2711
+ text: renderProbe(out)
2712
+ });
2713
+ }
2714
+ const unit = asText$1(args.unit);
2715
+ if (unit === "") throw new Error(`install 要一个 unit(工坊里:${modules.map((one) => one.unit).join(", ") || "(空)"})`);
2716
+ const found = modules.find((one) => one.unit === unit);
2717
+ if (found === void 0) throw new Error(`工坊里没有单元 id 为「${unit}」的模块(可用的:${modules.map((one) => one.unit).join(", ") || "(空)"})—— 先装上的前提是它在工坊里造好了`);
2718
+ if (!insideWorkshop(packagesDir, found.dir)) throw new Error(`这个模块不在工坊里(${found.dir})—— 只装 ${packagesDir} 之下的模块`);
2719
+ if (action === "open") {
2720
+ if (!found.installed) throw new Error(`${unit} 还没装 —— open 只负责上板。先用 action=install 装上(第一次会让用户点头一次)`);
2721
+ const panel = showOrReason(panels, unit, workspace);
2722
+ const text = panel.ok ? `面板上已经打开它了(${workspace})—— 让用户看一眼` : `面板上还没打开:${panel.reason}。这就是它的下一步(status 会给出同样的判断)`;
2723
+ return plainJson({
2724
+ workshop: packagesDir,
2725
+ unit,
2726
+ path: found.dir,
2727
+ panel,
2728
+ text
2729
+ });
2730
+ }
2731
+ if (found.installed) {
2732
+ const panel = showOrReason(panels, unit, workspace);
2733
+ const text = [`它已经装过了(就是工坊里这一份)—— **不要再装**:改源码 + dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面)。`, panel.ok ? `面板上已经打开它了(${workspace})—— 让用户看一眼` : `面板上还没打开:${panel.reason}。调 workbench_panels(action=show, id=${unit})`].join("\n");
2734
+ return plainJson({
2735
+ workshop: packagesDir,
2736
+ unit,
2737
+ path: found.dir,
2738
+ alreadyInstalled: true,
2739
+ panel,
2740
+ text
2741
+ });
2742
+ }
2743
+ const outcome = await market.installBundle(found.dir);
2744
+ const verdict = verdictOf(outcome.change);
2745
+ const restartRequired = outcome.change.application === "restart-required";
2746
+ const panel = outcome.change.application === "applied" ? showOrReason(panels, unit, workspace) : {
2747
+ ok: false,
2748
+ reason: `安装结果是 ${outcome.change.application},还没生效`
2749
+ };
2750
+ const panelText = panel.ok ? `面板上已经打开它了(${workspace})—— 让用户看一眼` : outcome.change.application === "applied" ? `面板上还没打开:${panel.reason}。调 workbench_panels(action=show, id=${unit})试试。` : restartRequired ? `面板上还没打开(${panel.reason}):模块要**重启一次工作台**才注册进面板 —— 把这句话转给用户,重启后再调 workbench_panels(action=show, id=${unit})。` : `面板上还没打开:${panel.reason}。先把上面那句"没装成"处理掉,再谈上板。`;
2751
+ const text = [
2752
+ `${verdict}:${unit}(${found.name})`,
2753
+ panelText,
2754
+ "之后只改源码 + dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面):不要再装一遍。"
2755
+ ].join("\n");
2756
+ return plainJson({
2757
+ workshop: packagesDir,
2758
+ unit,
2759
+ path: found.dir,
2760
+ install: outcome.change,
2761
+ module: outcome.module,
2762
+ panel,
2763
+ text
2764
+ });
2765
+ }
2766
+ };
2767
+ return [tools.register(tool)];
2768
+ }
2769
+ //#endregion
1555
2770
  //#region src/shared/api/host/release.ts
1556
2771
  function parseLedgerVersion(stdout, name) {
1557
2772
  const parsed = parseJsonLoose(stdout);
@@ -2428,7 +3643,8 @@ function panelsHandoffEntry(target = {}) {
2428
3643
  }
2429
3644
  function panelsFactsOf(call, manifests) {
2430
3645
  if (call.source === "workbench_panels") return actionOf(call) === "list" ? [PANELS_FACT] : void 0;
2431
- if (call.source !== "workbench_modules") return void 0;
3646
+ if (call.source === "workbench_diy" && actionOf(call) === "list") return [PANELS_FACT];
3647
+ if (call.source !== "workbench_modules" && call.source !== "workbench_diy") return void 0;
2432
3648
  if (actionOf(call) === "list") return [PANELS_FACT, CAPABILITY_FACT];
2433
3649
  const request = requestOf(call);
2434
3650
  if (request === void 0) return void 0;
@@ -2788,11 +4004,15 @@ var HOST_UNITS = [{
2788
4004
  });
2789
4005
  return {
2790
4006
  routes: panelRoutes(panels, values),
2791
- registerTools: () => [...registerPanelTools(ctx, panels, values), ...registerMarketTools(ctx, market)],
4007
+ registerTools: () => [
4008
+ ...registerPanelTools(ctx, panels, values),
4009
+ ...registerMarketTools(ctx, market),
4010
+ ...registerDiyTools(ctx, market, panels)
4011
+ ],
2792
4012
  factsOf: (call) => panelsFactsOf(call, manifests()),
2793
4013
  needsApproval: panelsApproval,
2794
4014
  gate: {
2795
- deniedTools: (workspace) => manifests().filter((manifest) => manifest.card !== null).filter((manifest) => !panels.isEnabled(manifest.id, workspace)).flatMap((manifest) => manifest.tools.map((tool) => tool.name)),
4015
+ deniedTools: (workspace) => [...manifests().filter((manifest) => manifest.card !== null).filter((manifest) => !panels.isEnabled(manifest.id, workspace)).flatMap((manifest) => manifest.tools.map((tool) => tool.name)), ...inWorkshopSession(ctx, workspace) ? [] : [DIY_TOOL]],
2796
4016
  subscribe: (listener) => panels.subscribe(listener)
2797
4017
  },
2798
4018
  prefs: {
@@ -2810,6 +4030,153 @@ function assembleHostUnits(registry) {
2810
4030
  for (const face of HOST_UNITS) registry.register(face);
2811
4031
  }
2812
4032
  //#endregion
4033
+ //#region src/shared/api/host/runtime-seed.ts
4034
+ var WORKSHOP_SKILLS_DIR = "workbench-module-diy";
4035
+ function runtimeSeedPaths(root) {
4036
+ const opencliRoot = join(root, "opencli");
4037
+ const opencliBinDir = join(opencliRoot, "bin");
4038
+ const workshop = join(root, "diy");
4039
+ return {
4040
+ root,
4041
+ entryDir: join(root, "bin"),
4042
+ opencliRoot,
4043
+ opencliBinDir,
4044
+ opencliBin: join(opencliBinDir, "opencli"),
4045
+ opencliHome: join(opencliRoot, "home"),
4046
+ workshop,
4047
+ workshopPackages: join(workshop, "packages"),
4048
+ workshopManifest: join(workshop, "package.json"),
4049
+ skillsLink: join(workshop, ".agents", "skills", WORKSHOP_SKILLS_DIR)
4050
+ };
4051
+ }
4052
+ function runtimeSeedRootOf(ctx) {
4053
+ const homePath = ctx.get("dshHomePath");
4054
+ if (typeof homePath !== "function") return null;
4055
+ try {
4056
+ return homePath("workbench");
4057
+ } catch {
4058
+ return null;
4059
+ }
4060
+ }
4061
+ function packageRootOf(moduleUrl) {
4062
+ return dirname(dirname(dirname(fileURLToPath(moduleUrl))));
4063
+ }
4064
+ function isInstalledCopy(packageRoot) {
4065
+ return packageRoot.split(sep).includes("node_modules");
4066
+ }
4067
+ function opencliWrapperScript(packageRoot, home) {
4068
+ const script = join(packageRoot, "bin", "opencli-isolated");
4069
+ return `#!/bin/sh\nexec env DSH_HOME=${quoteShellArg(home)} bash ${quoteShellArg(script)} "$@"\n`;
4070
+ }
4071
+ function workshopSeedPlan(packageRoot, home, root) {
4072
+ const script = join(packageRoot, "scripts", "seed-workshop.sh");
4073
+ const env = { DSH_HOME: home };
4074
+ const log = join(root, "seed.log");
4075
+ return {
4076
+ entry: {
4077
+ file: "bash",
4078
+ args: [script, "entry"],
4079
+ env,
4080
+ log
4081
+ },
4082
+ full: {
4083
+ file: "bash",
4084
+ args: [script],
4085
+ env,
4086
+ log
4087
+ }
4088
+ };
4089
+ }
4090
+ function runDetached(command, onError, spawnImpl = spawn) {
4091
+ let fd = null;
4092
+ try {
4093
+ fd = openSync(command.log, "a");
4094
+ const child = spawnImpl(command.file, [...command.args], {
4095
+ env: {
4096
+ ...process.env,
4097
+ ...command.env
4098
+ },
4099
+ detached: true,
4100
+ stdio: [
4101
+ "ignore",
4102
+ fd,
4103
+ fd
4104
+ ]
4105
+ });
4106
+ child.on("error", (error) => onError(`自举脚本没跑起来(${command.file}):${String(error)}`));
4107
+ child.unref();
4108
+ } catch (error) {
4109
+ onError(`自举脚本没跑起来(${command.file}):${String(error)}`);
4110
+ } finally {
4111
+ if (fd !== null) closeQuietly(fd);
4112
+ }
4113
+ }
4114
+ function closeQuietly(fd) {
4115
+ try {
4116
+ closeSync(fd);
4117
+ } catch {
4118
+ return;
4119
+ }
4120
+ }
4121
+ function prependPath(entryDir, env = process.env) {
4122
+ const parts = (env.PATH ?? "").split(delimiter).filter((part) => part !== "");
4123
+ if (parts.includes(entryDir)) return false;
4124
+ env.PATH = [entryDir, ...parts].join(delimiter);
4125
+ return true;
4126
+ }
4127
+ function seedRuntimeFiles(options) {
4128
+ const paths = runtimeSeedPaths(options.root);
4129
+ const done = [];
4130
+ try {
4131
+ for (const dir of [
4132
+ paths.entryDir,
4133
+ paths.opencliBinDir,
4134
+ paths.opencliHome,
4135
+ paths.workshopPackages
4136
+ ]) mkdirSync(dir, { recursive: true });
4137
+ const wanted = opencliWrapperScript(options.packageRoot, options.home);
4138
+ if (readText(paths.opencliBin) !== wanted) {
4139
+ rmSync(paths.opencliBin, { force: true });
4140
+ writeFileSync(paths.opencliBin, wanted, "utf8");
4141
+ chmodSync(paths.opencliBin, 493);
4142
+ done.push(paths.opencliBin);
4143
+ }
4144
+ if (linkSkills(paths.skillsLink, join(options.packageRoot, "skills", "workbench-module-diy"), options.log)) done.push(paths.skillsLink);
4145
+ } catch (error) {
4146
+ options.log?.(`自举没做完:${String(error)}`);
4147
+ }
4148
+ return done;
4149
+ }
4150
+ function readText(file) {
4151
+ try {
4152
+ return readFileSync(file, "utf8");
4153
+ } catch {
4154
+ return null;
4155
+ }
4156
+ }
4157
+ function readLink(link) {
4158
+ try {
4159
+ return readlinkSync(link);
4160
+ } catch {
4161
+ return null;
4162
+ }
4163
+ }
4164
+ function linkSkills(link, source, log) {
4165
+ if (!existsSync(source) || readLink(link) === source) return false;
4166
+ try {
4167
+ mkdirSync(dirname(link), { recursive: true });
4168
+ rmSync(link, {
4169
+ force: true,
4170
+ recursive: true
4171
+ });
4172
+ symlinkSync(source, link, "dir");
4173
+ return true;
4174
+ } catch (error) {
4175
+ log?.(`DIY 指引没挂上(${link}):${String(error)}`);
4176
+ return false;
4177
+ }
4178
+ }
4179
+ //#endregion
2813
4180
  //#region src/shared/api/host/events.ts
2814
4181
  var HEARTBEAT_MS = 25e3;
2815
4182
  function frame(payload) {
@@ -3003,7 +4370,8 @@ function moduleVersionsHandler(ctx, shell) {
3003
4370
  unit: module.unit,
3004
4371
  pkg: module.pkg,
3005
4372
  installed: module.version,
3006
- ...latest === void 0 ? {} : { latest }
4373
+ ...latest === void 0 ? {} : { latest },
4374
+ ...module.local === true ? { local: true } : {}
3007
4375
  };
3008
4376
  }))
3009
4377
  };
@@ -3045,18 +4413,6 @@ function bundleInspectHandler(ctx) {
3045
4413
  //#region src/shared/api/diy.ts
3046
4414
  var WORKSHOP_TITLE = "工作台DIY";
3047
4415
  //#endregion
3048
- //#region src/shared/api/host/diy.ts
3049
- var WORKSHOP_RELATIVE = "workbench/diy";
3050
- function workshopPathOf(ctx) {
3051
- const homePath = ctx.get("dshHomePath");
3052
- if (typeof homePath !== "function") return null;
3053
- try {
3054
- return homePath(WORKSHOP_RELATIVE);
3055
- } catch {
3056
- return null;
3057
- }
3058
- }
3059
- //#endregion
3060
4416
  //#region src/shared/api/dsh-plugins.ts
3061
4417
  var DSH_PLUGINS_ROUTE = "dsh-plugins";
3062
4418
  //#endregion
@@ -4087,11 +5443,37 @@ var inject = ["webServer"];
4087
5443
  function asArgs(value) {
4088
5444
  return typeof value === "object" && value !== null ? value : {};
4089
5445
  }
5446
+ function warn(ctx, message) {
5447
+ ctx.logger?.warn?.(`[dsh-workbench] ${message}`);
5448
+ }
5449
+ function seedWorkbenchRuntime(ctx) {
5450
+ const root = runtimeSeedRootOf(ctx);
5451
+ if (root === null) return;
5452
+ const home = dirname(root);
5453
+ const packageRoot = packageRootOf(import.meta.url);
5454
+ const paths = runtimeSeedPaths(root);
5455
+ const log = (message) => warn(ctx, message);
5456
+ for (const file of seedRuntimeFiles({
5457
+ root,
5458
+ home,
5459
+ packageRoot,
5460
+ log
5461
+ })) log(`自举写下 ${file}`);
5462
+ if (prependPath(paths.entryDir)) log(`PATH 前插 ${paths.entryDir}`);
5463
+ const plan = workshopSeedPlan(packageRoot, home, root);
5464
+ runDetached(plan.entry, log);
5465
+ if (isInstalledCopy(packageRoot) && !existsSync(paths.workshopManifest)) {
5466
+ log("DIY 工坊骨架不在:后台建起来(第一次会下载依赖,几百 MB)");
5467
+ runDetached(plan.full, log);
5468
+ }
5469
+ }
4090
5470
  function apply(ctx) {
4091
5471
  const facts = createFactLog();
4092
5472
  const values = createValueHandoff();
5473
+ seedWorkbenchRuntime(ctx);
5474
+ const opencli = opencliCommandOf(ctx);
4093
5475
  const routes = {
4094
- health: healthHandler(ctx),
5476
+ health: healthHandler(ctx, opencli),
4095
5477
  changes: changesHandler(facts),
4096
5478
  [DSH_PLUGINS_ROUTE]: dshPluginsHandler(ctx),
4097
5479
  [BUNDLE_REGISTRIES_ROUTE]: bundleRegistriesHandler(ctx),
@@ -4111,7 +5493,6 @@ function apply(ctx) {
4111
5493
  ctx.logger?.warn?.(`dsh-workbench: 工坊没能登记成工作区 —— ${String(error)}`);
4112
5494
  });
4113
5495
  });
4114
- const opencliEnv = process.env.DSH_WORKBENCH_OPENCLI;
4115
5496
  const installEnv = process.env.DSH_WORKBENCH_OPENCLI_INSTALL;
4116
5497
  const liveManifests = () => units.manifests();
4117
5498
  const manifestOf = (unit) => liveManifests().find((manifest) => manifest.id === unit);
@@ -4129,7 +5510,7 @@ function apply(ctx) {
4129
5510
  if (manifest === void 0 || !hasEnvNeeds(manifest)) return void 0;
4130
5511
  const found = (await probeUnits({
4131
5512
  shell,
4132
- ...opencliEnv === void 0 ? {} : { command: opencliEnv },
5513
+ command: opencli.command,
4133
5514
  force,
4134
5515
  units: [probeInput(manifest)]
4135
5516
  }))[0];
@@ -4176,7 +5557,7 @@ function apply(ctx) {
4176
5557
  logger: ctx.logger,
4177
5558
  manifests: liveManifests,
4178
5559
  store,
4179
- ...opencliEnv === void 0 ? {} : { opencliCommand: opencliEnv },
5560
+ opencliCommand: opencli.command,
4180
5561
  probe: (unit, options) => probeOne(unit, options?.force ?? false),
4181
5562
  values,
4182
5563
  longTasks: {
@@ -4269,7 +5650,7 @@ function apply(ctx) {
4269
5650
  else {
4270
5651
  routes.preflight = preflightHandler(createPreflight({
4271
5652
  shell,
4272
- ...opencliEnv === void 0 ? {} : { command: opencliEnv },
5653
+ command: opencli.command,
4273
5654
  ...installEnv === void 0 ? {} : { installCommand: installEnv },
4274
5655
  logger: ctx.logger,
4275
5656
  warmReads: () => [...mounted.values()].flatMap((item) => item.plan.warmReads())
@@ -4277,7 +5658,7 @@ function apply(ctx) {
4277
5658
  routes.capability = capabilityHandler(async (unit, force) => {
4278
5659
  if (unit === void 0) return { units: (await probeUnits({
4279
5660
  shell,
4280
- ...opencliEnv === void 0 ? {} : { command: opencliEnv },
5661
+ command: opencli.command,
4281
5662
  force,
4282
5663
  units: declaredNow().map(probeInput)
4283
5664
  })).map((item) => ({