dsh-plugin-discovery 1.1.1 → 1.2.4
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 +103 -95
- package/client.js +688 -21
- package/cordis.patch.yml +4 -0
- package/data/recommended.json +1206 -1206
- package/index.js +600 -14
- package/package.json +74 -74
package/index.js
CHANGED
|
@@ -14,6 +14,90 @@ const DEFAULT_CATALOG_GITEE_URL =
|
|
|
14
14
|
"https://gitee.com/hjhcos/dsh-plugin-discovery/raw/main/data/recommended.json";
|
|
15
15
|
const DEFAULT_CATALOG_GITHUB_URL =
|
|
16
16
|
"https://raw.githubusercontent.com/hjhcos/dsh-plugin-discovery/main/data/recommended.json";
|
|
17
|
+
const SELF_PACKAGE_NAME = "dsh-plugin-discovery";
|
|
18
|
+
const DEFAULT_SELF_PACKAGE_GITEE_URL =
|
|
19
|
+
"https://gitee.com/hjhcos/dsh-plugin-discovery/raw/main/package.json";
|
|
20
|
+
const DEFAULT_SELF_PACKAGE_GITHUB_URL =
|
|
21
|
+
"https://raw.githubusercontent.com/hjhcos/dsh-plugin-discovery/main/package.json";
|
|
22
|
+
|
|
23
|
+
function readLocalPackageMeta() {
|
|
24
|
+
try {
|
|
25
|
+
const raw = readFileSync(join(__dirname, "package.json"), "utf8");
|
|
26
|
+
const data = JSON.parse(raw);
|
|
27
|
+
return {
|
|
28
|
+
name: data.name || SELF_PACKAGE_NAME,
|
|
29
|
+
version: String(data.version || "0.0.0"),
|
|
30
|
+
};
|
|
31
|
+
} catch {
|
|
32
|
+
return { name: SELF_PACKAGE_NAME, version: "0.0.0" };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseSemverParts(version) {
|
|
37
|
+
const cleaned = String(version || "0")
|
|
38
|
+
.trim()
|
|
39
|
+
.replace(/^v/i, "")
|
|
40
|
+
.split("-")[0]
|
|
41
|
+
.split("+")[0];
|
|
42
|
+
return cleaned.split(".").map((part) => {
|
|
43
|
+
const n = Number.parseInt(part, 10);
|
|
44
|
+
return Number.isFinite(n) ? n : 0;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @returns {number} 1 if a>b, -1 if a<b, 0 if equal */
|
|
49
|
+
function compareSemver(a, b) {
|
|
50
|
+
const pa = parseSemverParts(a);
|
|
51
|
+
const pb = parseSemverParts(b);
|
|
52
|
+
const len = Math.max(pa.length, pb.length);
|
|
53
|
+
for (let i = 0; i < len; i += 1) {
|
|
54
|
+
const x = pa[i] || 0;
|
|
55
|
+
const y = pb[i] || 0;
|
|
56
|
+
if (x > y) return 1;
|
|
57
|
+
if (x < y) return -1;
|
|
58
|
+
}
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function fetchJsonWithTimeout(url, timeoutMs = 12000) {
|
|
63
|
+
const trimmed = String(url || "").trim();
|
|
64
|
+
if (!trimmed) return null;
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(trimmed, {
|
|
69
|
+
headers: {
|
|
70
|
+
Accept: "application/json,text/plain,*/*",
|
|
71
|
+
"User-Agent": USER_AGENT,
|
|
72
|
+
},
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
});
|
|
75
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
76
|
+
return await response.json();
|
|
77
|
+
} finally {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function fetchLatestSelfVersion(urls) {
|
|
83
|
+
const errors = [];
|
|
84
|
+
for (const entry of urls) {
|
|
85
|
+
try {
|
|
86
|
+
if (entry.kind === "npm") {
|
|
87
|
+
const data = await fetchJsonWithTimeout(entry.url);
|
|
88
|
+
const version = data?.version || data?.["dist-tags"]?.latest;
|
|
89
|
+
if (version) return { version: String(version), origin: "npm", url: entry.url };
|
|
90
|
+
} else {
|
|
91
|
+
const data = await fetchJsonWithTimeout(entry.url);
|
|
92
|
+
const version = data?.version;
|
|
93
|
+
if (version) return { version: String(version), origin: entry.label || "remote", url: entry.url };
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
errors.push(`${entry.label || entry.kind}: ${error instanceof Error ? error.message : String(error)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { version: null, origin: null, url: null, error: errors.join("; ") || "latest version unavailable" };
|
|
100
|
+
}
|
|
17
101
|
|
|
18
102
|
function emptyCatalog(error) {
|
|
19
103
|
return {
|
|
@@ -305,6 +389,171 @@ async function searchNpm(query, limit) {
|
|
|
305
389
|
});
|
|
306
390
|
}
|
|
307
391
|
|
|
392
|
+
/** 在线搜索时把精选目录命中置顶,避免同名冷门包(如 @dsh-ssh/dsh-ssh)盖过已验证的 @linxin666/dsh-ssh。 */
|
|
393
|
+
function catalogHitsForQuery(catalog, query, limit) {
|
|
394
|
+
const rows = filterCatalogPlugins(catalog, {
|
|
395
|
+
category: "",
|
|
396
|
+
query,
|
|
397
|
+
personalize: false,
|
|
398
|
+
installedNames: [],
|
|
399
|
+
});
|
|
400
|
+
return rows.slice(0, Math.max(0, limit)).map((row) => ({
|
|
401
|
+
...mapCatalogPlugin(row),
|
|
402
|
+
reason: "catalog",
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function suggestCatalogAlternatives(catalog, failedSpec, limit = 3) {
|
|
407
|
+
const failed = String(failedSpec || "").trim().toLowerCase();
|
|
408
|
+
if (!failed) return [];
|
|
409
|
+
const tokens = failed
|
|
410
|
+
.replace(/^@/, "")
|
|
411
|
+
.split(/[/@._-]+/)
|
|
412
|
+
.map((part) => part.trim().toLowerCase())
|
|
413
|
+
.filter((part) => part.length >= 3 && part !== "dsh" && part !== "plugin");
|
|
414
|
+
const uniq = [];
|
|
415
|
+
const seen = new Set([failed]);
|
|
416
|
+
for (const token of tokens.length ? tokens : [failed]) {
|
|
417
|
+
for (const row of catalogHitsForQuery(catalog, token, 8)) {
|
|
418
|
+
const spec = String(row.spec || "").trim();
|
|
419
|
+
if (!spec || seen.has(spec.toLowerCase())) continue;
|
|
420
|
+
seen.add(spec.toLowerCase());
|
|
421
|
+
uniq.push(spec);
|
|
422
|
+
if (uniq.length >= limit) return uniq;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return uniq;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** ManagementError 没有 message 字段,只有 code / diagnostic;再拼上 packageResult 输出尾部。 */
|
|
429
|
+
function formatChangeFailure(result, fallbackApplication = "failed") {
|
|
430
|
+
const parts = [];
|
|
431
|
+
const err = result?.error;
|
|
432
|
+
if (err && typeof err === "object") {
|
|
433
|
+
if (err.diagnostic) parts.push(String(err.diagnostic).trim());
|
|
434
|
+
if (err.code) parts.push(`错误码:${err.code}`);
|
|
435
|
+
if (Array.isArray(err.incompatible) && err.incompatible.length) {
|
|
436
|
+
const names = err.incompatible
|
|
437
|
+
.map((row) => `${row?.name || "?"}@${row?.version || "?"}`)
|
|
438
|
+
.filter(Boolean);
|
|
439
|
+
if (names.length) parts.push(`不兼容:${names.join("、")}`);
|
|
440
|
+
}
|
|
441
|
+
} else if (typeof err === "string" && err.trim()) {
|
|
442
|
+
parts.push(err.trim());
|
|
443
|
+
} else if (err?.message) {
|
|
444
|
+
parts.push(String(err.message).trim());
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const pkg = result?.packageResult;
|
|
448
|
+
if (pkg && typeof pkg === "object") {
|
|
449
|
+
if (pkg.kind) parts.push(`失败类型:${pkg.kind}`);
|
|
450
|
+
if (pkg.timedOut) parts.push("安装超时(长时间无输出)");
|
|
451
|
+
if (typeof pkg.exitCode === "number") parts.push(`退出码:${pkg.exitCode}`);
|
|
452
|
+
const output = String(pkg.output || "").trim();
|
|
453
|
+
if (output) {
|
|
454
|
+
const lines = output
|
|
455
|
+
.split(/\r?\n/)
|
|
456
|
+
.map((line) => line.trim())
|
|
457
|
+
.filter(Boolean);
|
|
458
|
+
const tail = lines.slice(-8).join("\n");
|
|
459
|
+
if (tail) parts.push(tail);
|
|
460
|
+
}
|
|
461
|
+
if (pkg.logPath) parts.push(`完整日志:${pkg.logPath}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (result?.failedAt) parts.push(`失败位置:${result.failedAt}`);
|
|
465
|
+
if (Array.isArray(result?.pendingBuilds) && result.pendingBuilds.length) {
|
|
466
|
+
parts.push(`待批准构建脚本:${result.pendingBuilds.join("、")}`);
|
|
467
|
+
}
|
|
468
|
+
if (Array.isArray(result?.warnings) && result.warnings.length) {
|
|
469
|
+
parts.push(`警告:${result.warnings.slice(0, 3).join(";")}`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const unique = [];
|
|
473
|
+
const seen = new Set();
|
|
474
|
+
for (const part of parts) {
|
|
475
|
+
const text = String(part || "").trim();
|
|
476
|
+
if (!text || seen.has(text)) continue;
|
|
477
|
+
seen.add(text);
|
|
478
|
+
unique.push(text);
|
|
479
|
+
}
|
|
480
|
+
if (unique.length) return unique.join("\n");
|
|
481
|
+
const application = result?.application || fallbackApplication;
|
|
482
|
+
return `安装失败(${application})。插件管理器未返回 diagnostic / 安装日志。`;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function extractIncompatibility(result) {
|
|
486
|
+
const code = result?.error?.code || null;
|
|
487
|
+
const incompatible = Array.isArray(result?.error?.incompatible)
|
|
488
|
+
? result.error.incompatible
|
|
489
|
+
.map((row) => ({
|
|
490
|
+
name: String(row?.name || "").trim(),
|
|
491
|
+
version: String(row?.version || "").trim(),
|
|
492
|
+
runtimeVersion: String(row?.runtimeVersion || "").trim(),
|
|
493
|
+
peers: row?.peers && typeof row.peers === "object" ? row.peers : {},
|
|
494
|
+
}))
|
|
495
|
+
.filter((row) => row.name && row.version)
|
|
496
|
+
: [];
|
|
497
|
+
return {
|
|
498
|
+
code,
|
|
499
|
+
incompatible,
|
|
500
|
+
needsRiskExemption: code === "incompatible-version" && incompatible.length > 0,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** 安装失败时从精选目录推荐相近/同标签插件,优先给出可替代的当前生态包。 */
|
|
505
|
+
function buildFailureRecommendations(catalog, failedSpec, limit = 4) {
|
|
506
|
+
const failed = String(failedSpec || "").trim();
|
|
507
|
+
const seen = new Set([failed.toLowerCase()]);
|
|
508
|
+
const out = [];
|
|
509
|
+
const pushRow = (row, reason) => {
|
|
510
|
+
const spec = String(row?.spec || "").trim();
|
|
511
|
+
if (!spec || seen.has(spec.toLowerCase())) return;
|
|
512
|
+
seen.add(spec.toLowerCase());
|
|
513
|
+
out.push({
|
|
514
|
+
spec,
|
|
515
|
+
title: row.title || spec,
|
|
516
|
+
description: row.description || "",
|
|
517
|
+
source: "catalog",
|
|
518
|
+
installSpec: spec,
|
|
519
|
+
catalogId: row.id || row.spec,
|
|
520
|
+
reason: reason || "similar",
|
|
521
|
+
featured: row.featured === true,
|
|
522
|
+
rank: typeof row.rank === "number" ? row.rank : 0,
|
|
523
|
+
});
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const plugins = Array.isArray(catalog?.plugins) ? catalog.plugins : [];
|
|
527
|
+
const failedRow = plugins.find((row) => String(row.spec || "").toLowerCase() === failed.toLowerCase());
|
|
528
|
+
if (failedRow) {
|
|
529
|
+
for (const rel of failedRow.related || []) {
|
|
530
|
+
const hit = plugins.find((row) => String(row.spec || "") === rel || String(row.id || "") === rel);
|
|
531
|
+
if (hit) pushRow(hit, "related");
|
|
532
|
+
if (out.length >= limit) return out;
|
|
533
|
+
}
|
|
534
|
+
for (const tag of failedRow.tags || []) {
|
|
535
|
+
for (const row of plugins) {
|
|
536
|
+
if ((row.tags || []).includes(tag)) pushRow(row, "tag");
|
|
537
|
+
if (out.length >= limit) return out;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
for (const cat of failedRow.categories || []) {
|
|
541
|
+
if (cat === "featured") continue;
|
|
542
|
+
for (const row of plugins) {
|
|
543
|
+
if ((row.categories || []).includes(cat)) pushRow(row, "category");
|
|
544
|
+
if (out.length >= limit) return out;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
for (const spec of suggestCatalogAlternatives(catalog, failed, limit)) {
|
|
550
|
+
const hit = plugins.find((row) => String(row.spec || "") === spec) || { spec, title: spec };
|
|
551
|
+
pushRow(hit, "similar");
|
|
552
|
+
if (out.length >= limit) break;
|
|
553
|
+
}
|
|
554
|
+
return out.slice(0, limit);
|
|
555
|
+
}
|
|
556
|
+
|
|
308
557
|
function mapGithubRepos(items) {
|
|
309
558
|
return (Array.isArray(items) ? items : []).map((repo) => ({
|
|
310
559
|
source: "github",
|
|
@@ -381,6 +630,14 @@ class PluginDiscoveryService extends TypertRemoteService {
|
|
|
381
630
|
catalogGithubUrl: z.string().default(DEFAULT_CATALOG_GITHUB_URL),
|
|
382
631
|
/** 远程检查间隔;期间复用内存/磁盘缓存 */
|
|
383
632
|
catalogRefreshTtlMs: z.number().min(0).max(86400000 * 7).default(3600000),
|
|
633
|
+
/** 启动与打开面板时检查本插件是否有新版本 */
|
|
634
|
+
selfUpdateCheckEnabled: z.boolean().default(true),
|
|
635
|
+
/** 发现新版本后自动执行更新(file: 本地开发安装会跳过自动更新) */
|
|
636
|
+
selfUpdateAuto: z.boolean().default(true),
|
|
637
|
+
selfUpdateCheckTtlMs: z.number().min(0).max(86400000 * 7).default(21600000),
|
|
638
|
+
selfPackageName: z.string().default(SELF_PACKAGE_NAME),
|
|
639
|
+
selfPackageGiteeUrl: z.string().default(DEFAULT_SELF_PACKAGE_GITEE_URL),
|
|
640
|
+
selfPackageGithubUrl: z.string().default(DEFAULT_SELF_PACKAGE_GITHUB_URL),
|
|
384
641
|
});
|
|
385
642
|
|
|
386
643
|
constructor(ctx, config) {
|
|
@@ -390,14 +647,206 @@ class PluginDiscoveryService extends TypertRemoteService {
|
|
|
390
647
|
this.catalog = pickNewerCatalog(loadBundledCatalog(), loadCachedCatalog());
|
|
391
648
|
this.catalogCheckedAt = 0;
|
|
392
649
|
this.catalogRefreshPromise = null;
|
|
650
|
+
this.selfUpdateCheckedAt = 0;
|
|
651
|
+
this.selfUpdateState = null;
|
|
652
|
+
this.selfUpdatePromise = null;
|
|
393
653
|
this.ctx.effect(() => this.registerTools());
|
|
394
654
|
// 启动后后台拉一次,不阻塞启动
|
|
395
655
|
this.ctx.effect(() => {
|
|
396
656
|
void this.refreshCatalog({ force: false }).catch(() => {});
|
|
657
|
+
void this.checkSelfUpdate({ force: false, auto: this.config.selfUpdateAuto === true }).catch(() => {});
|
|
397
658
|
return () => {};
|
|
398
659
|
});
|
|
399
660
|
}
|
|
400
661
|
|
|
662
|
+
getLocalInstallMeta() {
|
|
663
|
+
const local = readLocalPackageMeta();
|
|
664
|
+
const real = (() => {
|
|
665
|
+
try {
|
|
666
|
+
return join(__dirname);
|
|
667
|
+
} catch {
|
|
668
|
+
return "";
|
|
669
|
+
}
|
|
670
|
+
})();
|
|
671
|
+
const isFileInstall = /[\\/]bundles[\\/]dsh-plugin-discovery$/i.test(real.replace(/\\/g, "/"));
|
|
672
|
+
return {
|
|
673
|
+
name: this.config.selfPackageName || local.name || SELF_PACKAGE_NAME,
|
|
674
|
+
version: local.version,
|
|
675
|
+
isFileInstall,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
selfUpdateSourceUrls() {
|
|
680
|
+
const name = this.config.selfPackageName || SELF_PACKAGE_NAME;
|
|
681
|
+
const urls = [
|
|
682
|
+
{
|
|
683
|
+
kind: "npm",
|
|
684
|
+
label: "npm",
|
|
685
|
+
url: `https://registry.npmjs.org/${encodeURIComponent(name)}/latest`,
|
|
686
|
+
},
|
|
687
|
+
];
|
|
688
|
+
const gitee = String(this.config.selfPackageGiteeUrl || "").trim();
|
|
689
|
+
const github = String(this.config.selfPackageGithubUrl || "").trim();
|
|
690
|
+
if (gitee) urls.push({ kind: "json", label: "gitee", url: gitee });
|
|
691
|
+
if (github) urls.push({ kind: "json", label: "github", url: github });
|
|
692
|
+
return urls;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async resolveInstalledSelfVersion() {
|
|
696
|
+
const local = this.getLocalInstallMeta();
|
|
697
|
+
try {
|
|
698
|
+
const bundles = await this.ctx.pluginManager.listBundles();
|
|
699
|
+
const row = (Array.isArray(bundles) ? bundles : []).find((item) => item && item.name === local.name);
|
|
700
|
+
if (row?.version) {
|
|
701
|
+
return { currentVersion: String(row.version), source: "bundle", isFileInstall: local.isFileInstall, name: local.name };
|
|
702
|
+
}
|
|
703
|
+
} catch {
|
|
704
|
+
/* ignore */
|
|
705
|
+
}
|
|
706
|
+
return {
|
|
707
|
+
currentVersion: local.version,
|
|
708
|
+
source: "package",
|
|
709
|
+
isFileInstall: local.isFileInstall,
|
|
710
|
+
name: local.name,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async checkSelfUpdate(options = {}) {
|
|
715
|
+
if (!this.config.enabled) {
|
|
716
|
+
return { ok: false, message: "发现插件已停用" };
|
|
717
|
+
}
|
|
718
|
+
if (this.config.selfUpdateCheckEnabled === false) {
|
|
719
|
+
return { ok: true, skipped: true, reason: "check disabled", update: this.selfUpdateState };
|
|
720
|
+
}
|
|
721
|
+
const force = options.force === true;
|
|
722
|
+
const auto = options.auto === true;
|
|
723
|
+
const ttl = this.config.selfUpdateCheckTtlMs ?? 21600000;
|
|
724
|
+
if (!force && this.selfUpdateCheckedAt && Date.now() - this.selfUpdateCheckedAt < ttl && this.selfUpdateState) {
|
|
725
|
+
if (auto && this.selfUpdateState.updateAvailable && !this.selfUpdateState.isFileInstall) {
|
|
726
|
+
return this.updateSelf({ latestVersion: this.selfUpdateState.latestVersion });
|
|
727
|
+
}
|
|
728
|
+
return { ok: true, skipped: true, reason: "ttl", update: this.selfUpdateState };
|
|
729
|
+
}
|
|
730
|
+
if (this.selfUpdatePromise) return this.selfUpdatePromise;
|
|
731
|
+
|
|
732
|
+
this.selfUpdatePromise = (async () => {
|
|
733
|
+
const installed = await this.resolveInstalledSelfVersion();
|
|
734
|
+
const latestInfo = await fetchLatestSelfVersion(this.selfUpdateSourceUrls());
|
|
735
|
+
this.selfUpdateCheckedAt = Date.now();
|
|
736
|
+
if (!latestInfo.version) {
|
|
737
|
+
this.selfUpdateState = {
|
|
738
|
+
...installed,
|
|
739
|
+
latestVersion: null,
|
|
740
|
+
updateAvailable: false,
|
|
741
|
+
origin: null,
|
|
742
|
+
checkedAt: this.selfUpdateCheckedAt,
|
|
743
|
+
error: latestInfo.error,
|
|
744
|
+
};
|
|
745
|
+
return { ok: false, message: latestInfo.error || "无法获取最新版本", update: this.selfUpdateState };
|
|
746
|
+
}
|
|
747
|
+
const updateAvailable = compareSemver(latestInfo.version, installed.currentVersion) > 0;
|
|
748
|
+
this.selfUpdateState = {
|
|
749
|
+
name: installed.name,
|
|
750
|
+
currentVersion: installed.currentVersion,
|
|
751
|
+
latestVersion: latestInfo.version,
|
|
752
|
+
updateAvailable,
|
|
753
|
+
origin: latestInfo.origin,
|
|
754
|
+
isFileInstall: installed.isFileInstall,
|
|
755
|
+
checkedAt: this.selfUpdateCheckedAt,
|
|
756
|
+
};
|
|
757
|
+
if (auto && updateAvailable && !installed.isFileInstall && this.config.selfUpdateAuto !== false) {
|
|
758
|
+
const updated = await this.updateSelf({ latestVersion: latestInfo.version });
|
|
759
|
+
return { ...updated, update: this.selfUpdateState };
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
ok: true,
|
|
763
|
+
update: this.selfUpdateState,
|
|
764
|
+
message: updateAvailable
|
|
765
|
+
? `发现新版本 ${latestInfo.version}(当前 ${installed.currentVersion})`
|
|
766
|
+
: `已是最新版本 ${installed.currentVersion}`,
|
|
767
|
+
};
|
|
768
|
+
})().finally(() => {
|
|
769
|
+
this.selfUpdatePromise = null;
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
return this.selfUpdatePromise;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
async updateSelf(options = {}) {
|
|
776
|
+
if (!this.config.enabled) {
|
|
777
|
+
return { ok: false, message: "发现插件已停用" };
|
|
778
|
+
}
|
|
779
|
+
const installed = await this.resolveInstalledSelfVersion();
|
|
780
|
+
if (installed.isFileInstall) {
|
|
781
|
+
return {
|
|
782
|
+
ok: false,
|
|
783
|
+
skipped: true,
|
|
784
|
+
message: "当前为本地 file: 开发安装,已跳过自动更新。发布版用户会从 npm 升级。",
|
|
785
|
+
update: this.selfUpdateState,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
let latestVersion = typeof options.latestVersion === "string" ? options.latestVersion.trim() : "";
|
|
789
|
+
if (!latestVersion) {
|
|
790
|
+
const checked = await this.checkSelfUpdate({ force: true, auto: false });
|
|
791
|
+
latestVersion = checked?.update?.latestVersion || "";
|
|
792
|
+
if (!checked?.update?.updateAvailable) {
|
|
793
|
+
return {
|
|
794
|
+
ok: true,
|
|
795
|
+
updated: false,
|
|
796
|
+
message: checked?.message || "没有可更新版本",
|
|
797
|
+
update: checked?.update || this.selfUpdateState,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const name = installed.name || SELF_PACKAGE_NAME;
|
|
802
|
+
const spec = `${name}@${latestVersion}`;
|
|
803
|
+
const requestId =
|
|
804
|
+
globalThis.crypto?.randomUUID?.() || `pd-self-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
805
|
+
try {
|
|
806
|
+
const result = await this.ctx.pluginManager.installBundle(spec, {
|
|
807
|
+
requestId,
|
|
808
|
+
enabled: true,
|
|
809
|
+
});
|
|
810
|
+
const application = result?.application;
|
|
811
|
+
const failed = application === "failed" || application === "cancelled";
|
|
812
|
+
if (!failed) {
|
|
813
|
+
this.selfUpdateState = {
|
|
814
|
+
...(this.selfUpdateState || {}),
|
|
815
|
+
name,
|
|
816
|
+
currentVersion: latestVersion,
|
|
817
|
+
latestVersion,
|
|
818
|
+
updateAvailable: false,
|
|
819
|
+
origin: this.selfUpdateState?.origin || "npm",
|
|
820
|
+
isFileInstall: false,
|
|
821
|
+
checkedAt: Date.now(),
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
ok: !failed,
|
|
826
|
+
updated: !failed,
|
|
827
|
+
spec,
|
|
828
|
+
requestId,
|
|
829
|
+
application,
|
|
830
|
+
packageName: name,
|
|
831
|
+
version: latestVersion,
|
|
832
|
+
message: failed
|
|
833
|
+
? result?.error?.message || `更新失败(${application || "unknown"})`
|
|
834
|
+
: `已更新到 ${latestVersion},请刷新页面或重启 dsh web 使界面生效`,
|
|
835
|
+
update: this.selfUpdateState,
|
|
836
|
+
result,
|
|
837
|
+
};
|
|
838
|
+
} catch (error) {
|
|
839
|
+
return {
|
|
840
|
+
ok: false,
|
|
841
|
+
updated: false,
|
|
842
|
+
spec,
|
|
843
|
+
requestId,
|
|
844
|
+
message: error instanceof Error ? error.message : String(error),
|
|
845
|
+
update: this.selfUpdateState,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
401
850
|
getCatalog() {
|
|
402
851
|
return this.catalog || loadBundledCatalog();
|
|
403
852
|
}
|
|
@@ -573,6 +1022,14 @@ class PluginDiscoveryService extends TypertRemoteService {
|
|
|
573
1022
|
|
|
574
1023
|
// 打开推荐/分类时尝试按 TTL 拉远程;失败静默回退本地
|
|
575
1024
|
await this.refreshCatalog({ force: opts.refresh === true }).catch(() => {});
|
|
1025
|
+
const selfUpdate = await this.checkSelfUpdate({
|
|
1026
|
+
force: opts.refresh === true,
|
|
1027
|
+
auto: this.config.selfUpdateAuto === true,
|
|
1028
|
+
}).catch((error) => ({
|
|
1029
|
+
ok: false,
|
|
1030
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1031
|
+
update: this.selfUpdateState,
|
|
1032
|
+
}));
|
|
576
1033
|
|
|
577
1034
|
const catalog = this.getCatalog();
|
|
578
1035
|
const personalize = mode === "recommend";
|
|
@@ -594,6 +1051,9 @@ class PluginDiscoveryService extends TypertRemoteService {
|
|
|
594
1051
|
categories: catalog.categories,
|
|
595
1052
|
results,
|
|
596
1053
|
total: results.length,
|
|
1054
|
+
selfUpdate: selfUpdate?.update || this.selfUpdateState,
|
|
1055
|
+
selfUpdateMessage: selfUpdate?.message || null,
|
|
1056
|
+
selfUpdated: selfUpdate?.updated === true,
|
|
597
1057
|
};
|
|
598
1058
|
}
|
|
599
1059
|
|
|
@@ -641,6 +1101,9 @@ class PluginDiscoveryService extends TypertRemoteService {
|
|
|
641
1101
|
|
|
642
1102
|
const limit = this.config.maxCandidates;
|
|
643
1103
|
const token = opts.githubToken ?? this.config.githubToken;
|
|
1104
|
+
await this.refreshCatalog({ force: false }).catch(() => {});
|
|
1105
|
+
const catalog = this.getCatalog();
|
|
1106
|
+
const catalogHits = catalogHitsForQuery(catalog, q === "dsh" ? "" : q, limit);
|
|
644
1107
|
let raw = [];
|
|
645
1108
|
try {
|
|
646
1109
|
if (source === "npm" || source === "all") {
|
|
@@ -651,23 +1114,27 @@ class PluginDiscoveryService extends TypertRemoteService {
|
|
|
651
1114
|
}
|
|
652
1115
|
} catch (error) {
|
|
653
1116
|
this.ctx.logger.warn("[plugin-discovery] search failed", error);
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
1117
|
+
if (!catalogHits.length) {
|
|
1118
|
+
return {
|
|
1119
|
+
ok: false,
|
|
1120
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1121
|
+
results: [],
|
|
1122
|
+
total: 0,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
// 在线源失败时仍返回精选命中,避免「搜得到却全空」
|
|
1126
|
+
raw = [];
|
|
660
1127
|
}
|
|
661
1128
|
|
|
662
1129
|
const seen = new Set();
|
|
663
1130
|
const deduped = [];
|
|
664
|
-
for (const row of raw) {
|
|
1131
|
+
for (const row of [...catalogHits, ...raw]) {
|
|
665
1132
|
if (!row.spec || seen.has(row.spec)) continue;
|
|
666
1133
|
seen.add(row.spec);
|
|
667
1134
|
deduped.push({
|
|
668
1135
|
...row,
|
|
669
1136
|
installSpec: row.spec,
|
|
670
|
-
bundle: null,
|
|
1137
|
+
bundle: row.bundle ?? null,
|
|
671
1138
|
});
|
|
672
1139
|
if (deduped.length >= limit) break;
|
|
673
1140
|
}
|
|
@@ -791,6 +1258,17 @@ class PluginDiscoveryService extends TypertRemoteService {
|
|
|
791
1258
|
message: `「${existing}」已在 DSH 插件列表中,无需重复安装。请到「插件」页查看。`,
|
|
792
1259
|
};
|
|
793
1260
|
}
|
|
1261
|
+
const withInstallHint = (message) => {
|
|
1262
|
+
const text = String(message || "").trim() || "安装失败";
|
|
1263
|
+
const looksNetwork =
|
|
1264
|
+
/ERR_PNPM_META_FETCH_FAIL|fetch failed|ECONNRESET|ETIMEDOUT|ENOTFOUND|network|镜像/i.test(text);
|
|
1265
|
+
if (!looksNetwork) return text;
|
|
1266
|
+
const alts = suggestCatalogAlternatives(this.getCatalog(), trimmed);
|
|
1267
|
+
if (!alts.length) {
|
|
1268
|
+
return `${text}。若持续失败,可改用「分类浏览 / 为你推荐」里的精选包,或稍后重试。`;
|
|
1269
|
+
}
|
|
1270
|
+
return `${text}。可改试精选目录中的相近插件:${alts.join("、")}`;
|
|
1271
|
+
};
|
|
794
1272
|
try {
|
|
795
1273
|
const result = await this.ctx.pluginManager.installBundle(trimmed, {
|
|
796
1274
|
requestId,
|
|
@@ -799,6 +1277,18 @@ class PluginDiscoveryService extends TypertRemoteService {
|
|
|
799
1277
|
const packageName = result?.bundle || pre.name || trimmed;
|
|
800
1278
|
const application = result?.application;
|
|
801
1279
|
const failed = application === "failed" || application === "cancelled";
|
|
1280
|
+
const rawMessage = failed
|
|
1281
|
+
? formatChangeFailure(result, application || "failed")
|
|
1282
|
+
: `「${packageName}」安装完成(${application || "applied"} / ${result?.stage || "install"})`;
|
|
1283
|
+
const incompat = failed ? extractIncompatibility(result) : {};
|
|
1284
|
+
const recommendations =
|
|
1285
|
+
failed && (incompat.needsRiskExemption || incompat.code === "incompatible-version")
|
|
1286
|
+
? buildFailureRecommendations(this.getCatalog(), trimmed, 4)
|
|
1287
|
+
: undefined;
|
|
1288
|
+
let message = failed ? withInstallHint(rawMessage) : rawMessage;
|
|
1289
|
+
if (failed && recommendations && recommendations.length) {
|
|
1290
|
+
message = `${message}\n推荐改试:${recommendations.map((row) => row.spec).join("、")}`;
|
|
1291
|
+
}
|
|
802
1292
|
return {
|
|
803
1293
|
ok: !failed,
|
|
804
1294
|
alreadyInstalled: false,
|
|
@@ -806,15 +1296,65 @@ class PluginDiscoveryService extends TypertRemoteService {
|
|
|
806
1296
|
requestId,
|
|
807
1297
|
application,
|
|
808
1298
|
stage: result?.stage,
|
|
809
|
-
message
|
|
810
|
-
|
|
811
|
-
|
|
1299
|
+
message,
|
|
1300
|
+
detail: failed ? rawMessage : undefined,
|
|
1301
|
+
logPath: failed ? result?.packageResult?.logPath : undefined,
|
|
1302
|
+
code: incompat.code || undefined,
|
|
1303
|
+
incompatible: incompat.incompatible || undefined,
|
|
1304
|
+
needsRiskExemption: incompat.needsRiskExemption === true,
|
|
1305
|
+
recommendations,
|
|
1306
|
+
installSpec: trimmed,
|
|
812
1307
|
result,
|
|
813
1308
|
};
|
|
814
1309
|
} catch (error) {
|
|
815
1310
|
return {
|
|
816
1311
|
ok: false,
|
|
817
1312
|
requestId,
|
|
1313
|
+
message: withInstallHint(error instanceof Error ? error.message : String(error)),
|
|
1314
|
+
installSpec: trimmed,
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/** Typert Remote:参数必须是独立形参,不能解构、不能默认值、不能 rest。 */
|
|
1320
|
+
async grantVersionExemption(packageVersion, runtimeVersion, acceptRisk) {
|
|
1321
|
+
if (!this.config.enabled) {
|
|
1322
|
+
return { ok: false, message: "发现插件已停用" };
|
|
1323
|
+
}
|
|
1324
|
+
const pkg = String(packageVersion == null ? "" : packageVersion).trim();
|
|
1325
|
+
const runtime = String(runtimeVersion == null ? "" : runtimeVersion).trim();
|
|
1326
|
+
const accepted = acceptRisk === true;
|
|
1327
|
+
if (!pkg || !runtime) {
|
|
1328
|
+
return { ok: false, message: "缺少 packageVersion 或 runtimeVersion" };
|
|
1329
|
+
}
|
|
1330
|
+
if (!accepted) {
|
|
1331
|
+
return {
|
|
1332
|
+
ok: false,
|
|
1333
|
+
message: "未确认风险。授予版本豁免必须 acceptRisk=true,并先明确告知可能崩溃或数据丢失。",
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
if (typeof this.ctx.pluginManager?.setVersionExemption !== "function") {
|
|
1337
|
+
return { ok: false, message: "当前 DSH 不支持 setVersionExemption" };
|
|
1338
|
+
}
|
|
1339
|
+
try {
|
|
1340
|
+
const result = await this.ctx.pluginManager.setVersionExemption(pkg, runtime, true, true);
|
|
1341
|
+
const application = result?.application;
|
|
1342
|
+
const failed = application === "failed" || application === "cancelled";
|
|
1343
|
+
return {
|
|
1344
|
+
ok: !failed,
|
|
1345
|
+
packageVersion: pkg,
|
|
1346
|
+
runtimeVersion: runtime,
|
|
1347
|
+
application,
|
|
1348
|
+
message: failed
|
|
1349
|
+
? formatChangeFailure(result, application || "failed")
|
|
1350
|
+
: `已为 ${pkg} 在 DSH ${runtime} 上授予精确版本豁免,可重新安装。`,
|
|
1351
|
+
result,
|
|
1352
|
+
};
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
return {
|
|
1355
|
+
ok: false,
|
|
1356
|
+
packageVersion: pkg,
|
|
1357
|
+
runtimeVersion: runtime,
|
|
818
1358
|
message: error instanceof Error ? error.message : String(error),
|
|
819
1359
|
};
|
|
820
1360
|
}
|
|
@@ -829,6 +1369,22 @@ class PluginDiscoveryService extends TypertRemoteService {
|
|
|
829
1369
|
return { ok: true, settled: null, pending: true, message: "安装仍在进行或已丢失" };
|
|
830
1370
|
}
|
|
831
1371
|
const failed = settled.application === "failed" || settled.application === "cancelled";
|
|
1372
|
+
const failText = failed ? formatChangeFailure(settled, settled.application) : "";
|
|
1373
|
+
const incompat = failed ? extractIncompatibility(settled) : {};
|
|
1374
|
+
const failedSpec =
|
|
1375
|
+
(Array.isArray(incompat.incompatible) && incompat.incompatible[0]
|
|
1376
|
+
? `${incompat.incompatible[0].name}@${incompat.incompatible[0].version}`
|
|
1377
|
+
: "") || settled.bundle || "";
|
|
1378
|
+
const recommendations =
|
|
1379
|
+
failed && (incompat.needsRiskExemption || incompat.code === "incompatible-version")
|
|
1380
|
+
? buildFailureRecommendations(this.getCatalog(), failedSpec, 4)
|
|
1381
|
+
: undefined;
|
|
1382
|
+
let message = failed
|
|
1383
|
+
? failText
|
|
1384
|
+
: `安装完成(${settled.application} / ${settled.stage})`;
|
|
1385
|
+
if (failed && recommendations && recommendations.length) {
|
|
1386
|
+
message = `${message}\n推荐改试:${recommendations.map((row) => row.spec).join("、")}`;
|
|
1387
|
+
}
|
|
832
1388
|
return {
|
|
833
1389
|
ok: !failed,
|
|
834
1390
|
settled,
|
|
@@ -836,9 +1392,14 @@ class PluginDiscoveryService extends TypertRemoteService {
|
|
|
836
1392
|
packageName: settled.bundle,
|
|
837
1393
|
application: settled.application,
|
|
838
1394
|
stage: settled.stage,
|
|
839
|
-
message
|
|
840
|
-
|
|
841
|
-
|
|
1395
|
+
message,
|
|
1396
|
+
detail: failed ? failText : undefined,
|
|
1397
|
+
logPath: failed ? settled?.packageResult?.logPath : undefined,
|
|
1398
|
+
code: incompat.code || undefined,
|
|
1399
|
+
incompatible: incompat.incompatible || undefined,
|
|
1400
|
+
needsRiskExemption: incompat.needsRiskExemption === true,
|
|
1401
|
+
recommendations,
|
|
1402
|
+
installSpec: failedSpec || undefined,
|
|
842
1403
|
};
|
|
843
1404
|
} catch (error) {
|
|
844
1405
|
return {
|
|
@@ -929,6 +1490,28 @@ class PluginDiscoveryService extends TypertRemoteService {
|
|
|
929
1490
|
}),
|
|
930
1491
|
),
|
|
931
1492
|
);
|
|
1493
|
+
disposers.push(
|
|
1494
|
+
this.ctx.tools.register(
|
|
1495
|
+
defineTool({
|
|
1496
|
+
name: "plugin_discovery_check_update",
|
|
1497
|
+
description: "Check whether dsh-plugin-discovery has a newer npm/Gitee version and optionally update.",
|
|
1498
|
+
parameters: {
|
|
1499
|
+
autoUpdate: {
|
|
1500
|
+
type: "boolean",
|
|
1501
|
+
description: "If true and a newer version exists, install it now",
|
|
1502
|
+
},
|
|
1503
|
+
},
|
|
1504
|
+
output: toolOutput,
|
|
1505
|
+
async execute(args) {
|
|
1506
|
+
return service.checkSelfUpdate({
|
|
1507
|
+
force: true,
|
|
1508
|
+
auto: args.autoUpdate === true,
|
|
1509
|
+
});
|
|
1510
|
+
},
|
|
1511
|
+
presentCall: () => present("Check dsh-plugin-discovery update"),
|
|
1512
|
+
}),
|
|
1513
|
+
),
|
|
1514
|
+
);
|
|
932
1515
|
return () => {
|
|
933
1516
|
for (const dispose of disposers.reverse()) dispose();
|
|
934
1517
|
};
|
|
@@ -939,8 +1522,11 @@ class PluginDiscoveryService extends TypertRemoteService {
|
|
|
939
1522
|
|
|
940
1523
|
markRemoteMethod(PluginDiscoveryService.prototype, "search");
|
|
941
1524
|
markRemoteMethod(PluginDiscoveryService.prototype, "listCatalog");
|
|
1525
|
+
markRemoteMethod(PluginDiscoveryService.prototype, "checkSelfUpdate");
|
|
1526
|
+
markRemoteMethod(PluginDiscoveryService.prototype, "updateSelf");
|
|
942
1527
|
markRemoteMethod(PluginDiscoveryService.prototype, "resolve");
|
|
943
1528
|
markRemoteMethod(PluginDiscoveryService.prototype, "installPlugin");
|
|
1529
|
+
markRemoteMethod(PluginDiscoveryService.prototype, "grantVersionExemption");
|
|
944
1530
|
markRemoteMethod(PluginDiscoveryService.prototype, "listInstalled");
|
|
945
1531
|
markRemoteMethod(PluginDiscoveryService.prototype, "waitInstall");
|
|
946
1532
|
|