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