dsh-plugin-discovery 1.2.0 → 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.
Files changed (5) hide show
  1. package/README.md +103 -96
  2. package/client.js +496 -21
  3. package/data/recommended.json +1206 -1206
  4. package/index.js +281 -14
  5. package/package.json +74 -74
package/index.js CHANGED
@@ -389,6 +389,171 @@ async function searchNpm(query, limit) {
389
389
  });
390
390
  }
391
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
+
392
557
  function mapGithubRepos(items) {
393
558
  return (Array.isArray(items) ? items : []).map((repo) => ({
394
559
  source: "github",
@@ -936,6 +1101,9 @@ class PluginDiscoveryService extends TypertRemoteService {
936
1101
 
937
1102
  const limit = this.config.maxCandidates;
938
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);
939
1107
  let raw = [];
940
1108
  try {
941
1109
  if (source === "npm" || source === "all") {
@@ -946,23 +1114,27 @@ class PluginDiscoveryService extends TypertRemoteService {
946
1114
  }
947
1115
  } catch (error) {
948
1116
  this.ctx.logger.warn("[plugin-discovery] search failed", error);
949
- return {
950
- ok: false,
951
- message: error instanceof Error ? error.message : String(error),
952
- results: [],
953
- total: 0,
954
- };
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 = [];
955
1127
  }
956
1128
 
957
1129
  const seen = new Set();
958
1130
  const deduped = [];
959
- for (const row of raw) {
1131
+ for (const row of [...catalogHits, ...raw]) {
960
1132
  if (!row.spec || seen.has(row.spec)) continue;
961
1133
  seen.add(row.spec);
962
1134
  deduped.push({
963
1135
  ...row,
964
1136
  installSpec: row.spec,
965
- bundle: null,
1137
+ bundle: row.bundle ?? null,
966
1138
  });
967
1139
  if (deduped.length >= limit) break;
968
1140
  }
@@ -1086,6 +1258,17 @@ class PluginDiscoveryService extends TypertRemoteService {
1086
1258
  message: `「${existing}」已在 DSH 插件列表中,无需重复安装。请到「插件」页查看。`,
1087
1259
  };
1088
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
+ };
1089
1272
  try {
1090
1273
  const result = await this.ctx.pluginManager.installBundle(trimmed, {
1091
1274
  requestId,
@@ -1094,6 +1277,18 @@ class PluginDiscoveryService extends TypertRemoteService {
1094
1277
  const packageName = result?.bundle || pre.name || trimmed;
1095
1278
  const application = result?.application;
1096
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
+ }
1097
1292
  return {
1098
1293
  ok: !failed,
1099
1294
  alreadyInstalled: false,
@@ -1101,15 +1296,65 @@ class PluginDiscoveryService extends TypertRemoteService {
1101
1296
  requestId,
1102
1297
  application,
1103
1298
  stage: result?.stage,
1104
- message: failed
1105
- ? result?.error?.message || `安装失败(${application || "unknown"})`
1106
- : `「${packageName}」安装完成(${application || "applied"} / ${result?.stage || "install"})`,
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,
1107
1307
  result,
1108
1308
  };
1109
1309
  } catch (error) {
1110
1310
  return {
1111
1311
  ok: false,
1112
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,
1113
1358
  message: error instanceof Error ? error.message : String(error),
1114
1359
  };
1115
1360
  }
@@ -1124,6 +1369,22 @@ class PluginDiscoveryService extends TypertRemoteService {
1124
1369
  return { ok: true, settled: null, pending: true, message: "安装仍在进行或已丢失" };
1125
1370
  }
1126
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
+ }
1127
1388
  return {
1128
1389
  ok: !failed,
1129
1390
  settled,
@@ -1131,9 +1392,14 @@ class PluginDiscoveryService extends TypertRemoteService {
1131
1392
  packageName: settled.bundle,
1132
1393
  application: settled.application,
1133
1394
  stage: settled.stage,
1134
- message: failed
1135
- ? settled.error?.message || `安装失败(${settled.application})`
1136
- : `安装完成(${settled.application} / ${settled.stage})`,
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,
1137
1403
  };
1138
1404
  } catch (error) {
1139
1405
  return {
@@ -1260,6 +1526,7 @@ markRemoteMethod(PluginDiscoveryService.prototype, "checkSelfUpdate");
1260
1526
  markRemoteMethod(PluginDiscoveryService.prototype, "updateSelf");
1261
1527
  markRemoteMethod(PluginDiscoveryService.prototype, "resolve");
1262
1528
  markRemoteMethod(PluginDiscoveryService.prototype, "installPlugin");
1529
+ markRemoteMethod(PluginDiscoveryService.prototype, "grantVersionExemption");
1263
1530
  markRemoteMethod(PluginDiscoveryService.prototype, "listInstalled");
1264
1531
  markRemoteMethod(PluginDiscoveryService.prototype, "waitInstall");
1265
1532
 
package/package.json CHANGED
@@ -1,74 +1,74 @@
1
- {
2
- "name": "dsh-plugin-discovery",
3
- "version": "1.2.0",
4
- "type": "module",
5
- "description": "Discover and install community DSH bundles from the sidebar: newbie essentials, recommendations, categories, self-update, and npm/GitHub search.",
6
- "author": "hjhcos",
7
- "license": "MIT",
8
- "keywords": [
9
- "dsh",
10
- "dsh-plugin",
11
- "deepseek",
12
- "deepseek-harness",
13
- "plugin",
14
- "bundle",
15
- "discovery",
16
- "marketplace",
17
- "recommend"
18
- ],
19
- "homepage": "https://github.com/hjhcos/dsh-plugin-discovery#readme",
20
- "bugs": {
21
- "url": "https://github.com/hjhcos/dsh-plugin-discovery/issues"
22
- },
23
- "repository": {
24
- "type": "git",
25
- "url": "git+https://github.com/hjhcos/dsh-plugin-discovery.git"
26
- },
27
- "icon": "./icon.svg",
28
- "exports": {
29
- ".": "./index.js",
30
- "./client": "./client.js",
31
- "./package.json": "./package.json",
32
- "./locale/*.json": "./locale/*.json"
33
- },
34
- "files": [
35
- "index.js",
36
- "mark-remote.js",
37
- "client.js",
38
- "cordis.patch.yml",
39
- "icon.svg",
40
- "data",
41
- "locale",
42
- "skill",
43
- "docs",
44
- "LICENSE",
45
- "README.md"
46
- ],
47
- "dependencies": {
48
- "@deepseek-ai/cordis": "~4.0.4",
49
- "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
50
- "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
51
- "@deepseek-ai/schemastery": "~3.18.4"
52
- },
53
- "peerDependencies": {
54
- "@deepseek-ai/cordis": "~4.0.4",
55
- "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
56
- "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
57
- "@deepseek-ai/schemastery": "*"
58
- },
59
- "dsh": {
60
- "bundle": {
61
- "patch": "./cordis.patch.yml"
62
- },
63
- "client": {
64
- "platform": "web",
65
- "immediately": false,
66
- "inject": [
67
- "@deepseek-ai/dsh-client-ui-sidebar",
68
- "@deepseek-ai/dsh-client-ui-layout",
69
- "@deepseek-ai/dsh-client-locale",
70
- "@deepseek-ai/dsh-api-remotes"
71
- ]
72
- }
73
- }
74
- }
1
+ {
2
+ "name": "dsh-plugin-discovery",
3
+ "version": "1.2.4",
4
+ "type": "module",
5
+ "description": "Discover and install community DSH bundles from the sidebar: newbie essentials, recommendations, categories, self-update, and npm/GitHub search.",
6
+ "author": "hjhcos",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "dsh",
10
+ "dsh-plugin",
11
+ "deepseek",
12
+ "deepseek-harness",
13
+ "plugin",
14
+ "bundle",
15
+ "discovery",
16
+ "marketplace",
17
+ "recommend"
18
+ ],
19
+ "homepage": "https://github.com/hjhcos/dsh-plugin-discovery#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/hjhcos/dsh-plugin-discovery/issues"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/hjhcos/dsh-plugin-discovery.git"
26
+ },
27
+ "icon": "./icon.svg",
28
+ "exports": {
29
+ ".": "./index.js",
30
+ "./client": "./client.js",
31
+ "./package.json": "./package.json",
32
+ "./locale/*.json": "./locale/*.json"
33
+ },
34
+ "files": [
35
+ "index.js",
36
+ "mark-remote.js",
37
+ "client.js",
38
+ "cordis.patch.yml",
39
+ "icon.svg",
40
+ "data",
41
+ "locale",
42
+ "skill",
43
+ "docs",
44
+ "LICENSE",
45
+ "README.md"
46
+ ],
47
+ "dependencies": {
48
+ "@deepseek-ai/cordis": "~4.0.4",
49
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
50
+ "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
51
+ "@deepseek-ai/schemastery": "~3.18.4"
52
+ },
53
+ "peerDependencies": {
54
+ "@deepseek-ai/cordis": "~4.0.4",
55
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
56
+ "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
57
+ "@deepseek-ai/schemastery": "*"
58
+ },
59
+ "dsh": {
60
+ "bundle": {
61
+ "patch": "./cordis.patch.yml"
62
+ },
63
+ "client": {
64
+ "platform": "web",
65
+ "immediately": false,
66
+ "inject": [
67
+ "@deepseek-ai/dsh-client-ui-sidebar",
68
+ "@deepseek-ai/dsh-client-ui-layout",
69
+ "@deepseek-ai/dsh-client-locale",
70
+ "@deepseek-ai/dsh-api-remotes"
71
+ ]
72
+ }
73
+ }
74
+ }