@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.
- package/README.md +19 -0
- package/bin/dsh-workbench +21 -1
- package/client.js +163 -89
- package/cordis.patch.yml +40 -0
- package/dist/client/client.body.js +163 -89
- package/dist/client/index.js +163 -89
- package/dist/host/index.js +1474 -93
- package/dist/types/app/__tests__/workbench-styles.test.d.ts +1 -0
- package/dist/types/app/client.d.ts +39 -15
- package/dist/types/app/layouts/rightbar/board-columns.d.ts +8 -0
- package/dist/types/app/layouts/rightbar/board-columns.test.d.ts +1 -0
- package/dist/types/app/layouts/rightbar/market-rows.d.ts +2 -0
- package/dist/types/app/layouts/workbench-styles.d.ts +2 -0
- package/dist/types/modules/panels/__tests__/diy-harness.d.ts +51 -0
- package/dist/types/modules/panels/__tests__/diy-tools-status.test.d.ts +1 -0
- package/dist/types/modules/panels/__tests__/diy-tools.test.d.ts +1 -0
- package/dist/types/modules/panels/model/diy-tools.d.ts +4 -0
- package/dist/types/modules/panels/unit.d.ts +1 -0
- package/dist/types/shared/api/bundle-install.d.ts +1 -0
- package/dist/types/shared/api/host/diy-facts.d.ts +9 -0
- package/dist/types/shared/api/host/diy-facts.test.d.ts +1 -0
- package/dist/types/shared/api/host/diy-recon-fs.d.ts +3 -0
- package/dist/types/shared/api/host/diy-recon-fs.test.d.ts +1 -0
- package/dist/types/shared/api/host/diy-recon.d.ts +39 -0
- package/dist/types/shared/api/host/diy-recon.test.d.ts +1 -0
- package/dist/types/shared/api/host/diy-workshop.d.ts +12 -0
- package/dist/types/shared/api/host/diy-workshop.test.d.ts +1 -0
- package/dist/types/shared/api/host/installed-module.d.ts +2 -0
- package/dist/types/shared/api/host/opencli-command.d.ts +10 -0
- package/dist/types/shared/api/host/opencli-command.test.d.ts +1 -0
- package/dist/types/shared/api/host/runtime-seed.d.ts +40 -0
- package/dist/types/shared/api/host/runtime-seed.test.d.ts +1 -0
- package/docs/HANDOFF.md +82 -44
- package/docs/OPERATIONS.md +22 -1
- package/package.json +5 -5
- package/scripts/seed-profile.sh +4 -113
- package/scripts/seed-workshop.sh +216 -0
- package/skills/workbench-module-diy/BUILD.md +36 -66
- package/skills/workbench-module-diy/DATASOURCE.md +80 -12
- package/skills/workbench-module-diy/DESIGN.md +27 -2
- package/skills/workbench-module-diy/LINKAGE.md +49 -19
- package/skills/workbench-module-diy/PANEL.md +78 -4
- package/skills/workbench-module-diy/REVIEW.md +6 -5
- package/skills/workbench-module-diy/SKILL.md +32 -12
- package/skills/workbench-module-diy/STEPS.md +4 -0
- package/skills/workbench-module-diy/TOOLING.md +47 -0
- package/skills/workbench-module-diy/TOOLS.md +147 -5
- package/skills/workbench-module-diy/WIRING.md +88 -0
package/dist/host/index.js
CHANGED
|
@@ -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$
|
|
689
|
+
function asRecord$5(value) {
|
|
201
690
|
return typeof value === "object" && value !== null ? value : {};
|
|
202
691
|
}
|
|
203
|
-
function stringsOf$
|
|
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$
|
|
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$
|
|
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$
|
|
1620
|
+
function asRecord$4(value) {
|
|
1122
1621
|
return typeof value === "object" && value !== null ? value : void 0;
|
|
1123
1622
|
}
|
|
1124
|
-
function asText$
|
|
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$
|
|
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$
|
|
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$
|
|
1653
|
+
output: asText$4(record.output),
|
|
1155
1654
|
truncated: record.truncated === true,
|
|
1156
|
-
logPath: asText$
|
|
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$
|
|
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$
|
|
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$
|
|
1171
|
-
const errorCode = error === void 0 ? "" : asText$
|
|
1172
|
-
const errorDetail = error === void 0 ? "" : asText$
|
|
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$
|
|
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$
|
|
1686
|
+
const record = asRecord$4(raw);
|
|
1188
1687
|
if (record === void 0) return null;
|
|
1189
|
-
const status = asText$
|
|
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$
|
|
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$
|
|
1205
|
-
const version = asText$
|
|
1206
|
-
const description = asText$
|
|
1207
|
-
const host = asText$
|
|
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$
|
|
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$
|
|
1751
|
+
function asRecord$3(value) {
|
|
1253
1752
|
return typeof value === "object" && value !== null ? value : void 0;
|
|
1254
1753
|
}
|
|
1255
|
-
function asText$
|
|
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$
|
|
1776
|
+
const record = asRecord$3(manifest);
|
|
1278
1777
|
if (record === void 0) return [];
|
|
1279
|
-
const bundles = stringsOf(asRecord$
|
|
1280
|
-
const
|
|
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$
|
|
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$
|
|
1332
|
-
const unit = declared === void 0 ? "" : asText$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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([
|
|
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
|
|
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: () => [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5661
|
+
command: opencli.command,
|
|
4281
5662
|
force,
|
|
4282
5663
|
units: declaredNow().map(probeInput)
|
|
4283
5664
|
})).map((item) => ({
|