dsh-plugin-discovery 1.1.0 → 1.2.0

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 CHANGED
@@ -54,6 +54,7 @@ github:hjhcos/dsh-plugin-discovery
54
54
  - **个性化推荐**:按已装插件偏好排序
55
55
  - **分类浏览**:桌宠 / 主题 / IM / 记忆 / 效率 / 多媒体 / 远程 / 市场 / 多智能体
56
56
  - **远程目录更新**:优先 Gitee raw JSON,失败再试 GitHub;按 `version` 写入 `$DSH_HOME/cache/...`
57
+ - **插件自更新**:启动/打开面板时检查 npm 最新版(Gitee `package.json` 兜底);有新版本可自动或一键更新
57
58
  - 关键词搜索(全部 / npm / GitHub)与链接解析安装
58
59
  - 安装三态:未安装 / 安装中 / 已安装
59
60
  - 中英文界面
package/client.js CHANGED
@@ -64,6 +64,17 @@ window.__ModuleLoader__.load({
64
64
  catalogOriginGithub: "GitHub",
65
65
  catalogOriginRemote: "远程",
66
66
  refreshCatalog: "刷新目录",
67
+ selfUpdateTitle: "发现插件有新版本",
68
+ selfUpdateDesc: "当前 {current} → 最新 {latest}(来源 {origin})",
69
+ selfUpdateNow: "立即更新",
70
+ selfUpdateChecking: "正在检查更新…",
71
+ selfUpdateUpdating: "正在更新…",
72
+ selfUpdateLatest: "发现插件已是最新版 {version}",
73
+ selfUpdateDone: "已更新到 {version},请刷新页面",
74
+ selfUpdateFileSkip: "本地开发安装,已跳过自动更新",
75
+ selfUpdateOriginNpm: "npm",
76
+ selfUpdateOriginGitee: "Gitee",
77
+ selfUpdateOriginGithub: "GitHub",
67
78
  reasonFeatured: "精选",
68
79
  reasonRelated: "与已装相关",
69
80
  reasonPopular: "热门",
@@ -133,6 +144,17 @@ window.__ModuleLoader__.load({
133
144
  catalogOriginGithub: "GitHub",
134
145
  catalogOriginRemote: "remote",
135
146
  refreshCatalog: "Refresh catalog",
147
+ selfUpdateTitle: "Discovery plugin update available",
148
+ selfUpdateDesc: "Current {current} → latest {latest} (via {origin})",
149
+ selfUpdateNow: "Update now",
150
+ selfUpdateChecking: "Checking for updates…",
151
+ selfUpdateUpdating: "Updating…",
152
+ selfUpdateLatest: "Discovery plugin is up to date ({version})",
153
+ selfUpdateDone: "Updated to {version}. Please refresh the page.",
154
+ selfUpdateFileSkip: "Local file: install — auto-update skipped",
155
+ selfUpdateOriginNpm: "npm",
156
+ selfUpdateOriginGitee: "Gitee",
157
+ selfUpdateOriginGithub: "GitHub",
136
158
  reasonFeatured: "Featured",
137
159
  reasonRelated: "Related to installed",
138
160
  reasonPopular: "Popular",
@@ -581,6 +603,27 @@ window.__ModuleLoader__.load({
581
603
  color: "var(--dsw-alias-state-warn-primary, var(--dsw-alias-label-secondary))",
582
604
  fontSize: "13px",
583
605
  },
606
+ updateBox: {
607
+ display: "flex",
608
+ alignItems: "center",
609
+ justifyContent: "space-between",
610
+ gap: "12px",
611
+ flexWrap: "wrap",
612
+ padding: "12px 14px",
613
+ borderRadius: "var(--dsw-radius-sm, 8px)",
614
+ border: "1px solid color-mix(in srgb, var(--dsw-alias-state-business-primary, #4176e6) 35%, transparent)",
615
+ background: "color-mix(in srgb, var(--dsw-alias-state-business-primary, #4176e6) 8%, transparent)",
616
+ color: "var(--dsw-alias-label-primary)",
617
+ fontSize: "13px",
618
+ lineHeight: "20px",
619
+ },
620
+ updateText: {
621
+ display: "flex",
622
+ flexDirection: "column",
623
+ gap: "2px",
624
+ margin: 0,
625
+ flex: "1 1 220px",
626
+ },
584
627
  pager: {
585
628
  display: "flex",
586
629
  alignItems: "center",
@@ -725,6 +768,8 @@ window.__ModuleLoader__.load({
725
768
  const [category, setCategory] = React.useState("beginner");
726
769
  const [categories, setCategories] = React.useState([]);
727
770
  const [catalogInfo, setCatalogInfo] = React.useState({ version: 0, origin: "bundled" });
771
+ const [selfUpdate, setSelfUpdate] = React.useState(null);
772
+ const [selfUpdateBusy, setSelfUpdateBusy] = React.useState(false);
728
773
  const [query, setQuery] = React.useState(DEFAULT_QUERY);
729
774
  const [source, setSource] = React.useState("all");
730
775
  const [results, setResults] = React.useState([]);
@@ -866,6 +911,10 @@ window.__ModuleLoader__.load({
866
911
  version: res.catalogVersion || 0,
867
912
  origin: res.catalogOrigin || "bundled",
868
913
  });
914
+ if (res.selfUpdate) setSelfUpdate(res.selfUpdate);
915
+ if (res.selfUpdated && res.selfUpdate?.latestVersion) {
916
+ setMessage(format(t("selfUpdateDone"), { version: res.selfUpdate.latestVersion }));
917
+ }
869
918
  const list = res.results || [];
870
919
  setResults(list);
871
920
  setPage(1);
@@ -1228,6 +1277,10 @@ window.__ModuleLoader__.load({
1228
1277
  version: res.catalogVersion || 0,
1229
1278
  origin: res.catalogOrigin || "bundled",
1230
1279
  });
1280
+ if (res.selfUpdate) setSelfUpdate(res.selfUpdate);
1281
+ if (res.selfUpdated && res.selfUpdate?.latestVersion) {
1282
+ setMessage(format(t("selfUpdateDone"), { version: res.selfUpdate.latestVersion }));
1283
+ }
1231
1284
  setResults(res.results || []);
1232
1285
  setPage(1);
1233
1286
  }
@@ -1238,6 +1291,48 @@ window.__ModuleLoader__.load({
1238
1291
  })();
1239
1292
  };
1240
1293
 
1294
+ const onUpdateSelf = () => {
1295
+ void (async () => {
1296
+ if (!api || typeof api.updateSelf !== "function") return;
1297
+ setSelfUpdateBusy(true);
1298
+ setError("");
1299
+ try {
1300
+ const res = await api.updateSelf({
1301
+ latestVersion: selfUpdate && selfUpdate.latestVersion,
1302
+ });
1303
+ if (res && res.update) setSelfUpdate(res.update);
1304
+ if (res && res.ok && res.updated) {
1305
+ setMessage(format(t("selfUpdateDone"), { version: res.version || res.update?.latestVersion }));
1306
+ setSelfUpdate((prev) =>
1307
+ prev
1308
+ ? {
1309
+ ...prev,
1310
+ updateAvailable: false,
1311
+ currentVersion: res.version || prev.latestVersion,
1312
+ latestVersion: res.version || prev.latestVersion,
1313
+ }
1314
+ : prev,
1315
+ );
1316
+ } else if (res && res.skipped) {
1317
+ setMessage(res.message || t("selfUpdateFileSkip"));
1318
+ } else {
1319
+ setError((res && res.message) || t("installFailed"));
1320
+ }
1321
+ } catch (e) {
1322
+ setError((e && e.message) || String(e));
1323
+ } finally {
1324
+ setSelfUpdateBusy(false);
1325
+ }
1326
+ })();
1327
+ };
1328
+
1329
+ const selfUpdateOriginLabel =
1330
+ selfUpdate && selfUpdate.origin === "gitee"
1331
+ ? t("selfUpdateOriginGitee")
1332
+ : selfUpdate && selfUpdate.origin === "github"
1333
+ ? t("selfUpdateOriginGithub")
1334
+ : t("selfUpdateOriginNpm");
1335
+
1241
1336
  return h(
1242
1337
  "div",
1243
1338
  { style: styles.page },
@@ -1250,6 +1345,36 @@ window.__ModuleLoader__.load({
1250
1345
  h("h1", { style: styles.title }, t("panel")),
1251
1346
  h("p", { style: styles.intro }, t("intro")),
1252
1347
  ),
1348
+ selfUpdate && selfUpdate.updateAvailable
1349
+ ? h(
1350
+ "div",
1351
+ { style: styles.updateBox },
1352
+ h(
1353
+ "div",
1354
+ { style: styles.updateText },
1355
+ h("strong", null, t("selfUpdateTitle")),
1356
+ h(
1357
+ "span",
1358
+ null,
1359
+ format(t("selfUpdateDesc"), {
1360
+ current: selfUpdate.currentVersion || "?",
1361
+ latest: selfUpdate.latestVersion || "?",
1362
+ origin: selfUpdateOriginLabel,
1363
+ }),
1364
+ ),
1365
+ selfUpdate.isFileInstall ? h("span", null, t("selfUpdateFileSkip")) : null,
1366
+ ),
1367
+ h(
1368
+ Btn,
1369
+ {
1370
+ size: "sm",
1371
+ disabled: selfUpdateBusy || busy || !!selfUpdate.isFileInstall,
1372
+ onClick: onUpdateSelf,
1373
+ },
1374
+ selfUpdateBusy ? t("selfUpdateUpdating") : t("selfUpdateNow"),
1375
+ ),
1376
+ )
1377
+ : null,
1253
1378
  h(
1254
1379
  "div",
1255
1380
  { style: styles.card },
@@ -1517,6 +1642,10 @@ window.__ModuleLoader__.load({
1517
1642
  const SEARCH_RESULT = strictJson("dsh-plugin-discovery#pluginDiscovery/search:result");
1518
1643
  const CATALOG_OPTIONS_CODEC = strictJson("dsh-plugin-discovery#pluginDiscovery/listCatalog:options");
1519
1644
  const CATALOG_RESULT = strictJson("dsh-plugin-discovery#pluginDiscovery/listCatalog:result");
1645
+ const SELF_UPDATE_OPTIONS = strictJson("dsh-plugin-discovery#pluginDiscovery/checkSelfUpdate:options");
1646
+ const SELF_UPDATE_RESULT = strictJson("dsh-plugin-discovery#pluginDiscovery/checkSelfUpdate:result");
1647
+ const SELF_UPDATE_APPLY_OPTIONS = strictJson("dsh-plugin-discovery#pluginDiscovery/updateSelf:options");
1648
+ const SELF_UPDATE_APPLY_RESULT = strictJson("dsh-plugin-discovery#pluginDiscovery/updateSelf:result");
1520
1649
  const INPUT_CODEC = strictJson("dsh-plugin-discovery#pluginDiscovery/resolve:input");
1521
1650
  const RESOLVE_RESULT = strictJson("dsh-plugin-discovery#pluginDiscovery/resolve:result");
1522
1651
  const SPEC_CODEC = strictJson("dsh-plugin-discovery#pluginDiscovery/installPlugin:spec");
@@ -1564,6 +1693,40 @@ window.__ModuleLoader__.load({
1564
1693
  ],
1565
1694
  result: CATALOG_RESULT,
1566
1695
  },
1696
+ {
1697
+ id: "dsh-plugin-discovery#pluginDiscovery/checkSelfUpdate",
1698
+ service: "pluginDiscovery",
1699
+ namespace: "pluginDiscovery",
1700
+ method: "checkSelfUpdate",
1701
+ invocation: { kind: "direct" },
1702
+ parameters: [
1703
+ {
1704
+ name: "options",
1705
+ wire: "options",
1706
+ source: "json",
1707
+ acceptsUndefined: true,
1708
+ codec: SELF_UPDATE_OPTIONS,
1709
+ },
1710
+ ],
1711
+ result: SELF_UPDATE_RESULT,
1712
+ },
1713
+ {
1714
+ id: "dsh-plugin-discovery#pluginDiscovery/updateSelf",
1715
+ service: "pluginDiscovery",
1716
+ namespace: "pluginDiscovery",
1717
+ method: "updateSelf",
1718
+ invocation: { kind: "direct" },
1719
+ parameters: [
1720
+ {
1721
+ name: "options",
1722
+ wire: "options",
1723
+ source: "json",
1724
+ acceptsUndefined: true,
1725
+ codec: SELF_UPDATE_APPLY_OPTIONS,
1726
+ },
1727
+ ],
1728
+ result: SELF_UPDATE_APPLY_RESULT,
1729
+ },
1567
1730
  {
1568
1731
  id: "dsh-plugin-discovery#pluginDiscovery/resolve",
1569
1732
  service: "pluginDiscovery",
@@ -1657,6 +1820,35 @@ window.__ModuleLoader__.load({
1657
1820
  }
1658
1821
  return value;
1659
1822
  },
1823
+ checkSelfUpdate: async (opts) => {
1824
+ if (mountError) return { ok: false, message: mountError };
1825
+ const remote = remoteNs();
1826
+ if (!remote || typeof remote.checkSelfUpdate !== "function") {
1827
+ return { ok: false, message: "remote.pluginDiscovery.checkSelfUpdate unavailable" };
1828
+ }
1829
+ const wire = await remote.checkSelfUpdate(opts || {});
1830
+ const value = unwrapRemote(wire, "checkSelfUpdate failed");
1831
+ if (wire && wire.ok === false) return { ok: false, message: value.message, update: value.update };
1832
+ return value;
1833
+ },
1834
+ updateSelf: async (opts) => {
1835
+ if (mountError) return { ok: false, message: mountError };
1836
+ const remote = remoteNs();
1837
+ if (!remote || typeof remote.updateSelf !== "function") {
1838
+ return { ok: false, message: "remote.pluginDiscovery.updateSelf unavailable" };
1839
+ }
1840
+ const wire = await remote.updateSelf(opts || {});
1841
+ const value = unwrapRemote(wire, "updateSelf failed");
1842
+ if (wire && wire.ok === false) {
1843
+ return {
1844
+ ok: false,
1845
+ message: value.message,
1846
+ skipped: !!(value && value.skipped),
1847
+ update: value.update,
1848
+ };
1849
+ }
1850
+ return value;
1851
+ },
1660
1852
  resolve: async (input) => {
1661
1853
  if (mountError) return { ok: false, message: mountError };
1662
1854
  const remote = remoteNs();
package/cordis.patch.yml CHANGED
@@ -13,3 +13,7 @@
13
13
  catalogGiteeUrl: 'https://gitee.com/hjhcos/dsh-plugin-discovery/raw/main/data/recommended.json'
14
14
  catalogGithubUrl: 'https://raw.githubusercontent.com/hjhcos/dsh-plugin-discovery/main/data/recommended.json'
15
15
  catalogRefreshTtlMs: 3600000
16
+ # 本插件自动检查 npm 最新版(Gitee package.json 兜底);非 file: 安装时可自动更新
17
+ selfUpdateCheckEnabled: true
18
+ selfUpdateAuto: true
19
+ selfUpdateCheckTtlMs: 21600000
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 {
@@ -381,6 +465,14 @@ class PluginDiscoveryService extends TypertRemoteService {
381
465
  catalogGithubUrl: z.string().default(DEFAULT_CATALOG_GITHUB_URL),
382
466
  /** 远程检查间隔;期间复用内存/磁盘缓存 */
383
467
  catalogRefreshTtlMs: z.number().min(0).max(86400000 * 7).default(3600000),
468
+ /** 启动与打开面板时检查本插件是否有新版本 */
469
+ selfUpdateCheckEnabled: z.boolean().default(true),
470
+ /** 发现新版本后自动执行更新(file: 本地开发安装会跳过自动更新) */
471
+ selfUpdateAuto: z.boolean().default(true),
472
+ selfUpdateCheckTtlMs: z.number().min(0).max(86400000 * 7).default(21600000),
473
+ selfPackageName: z.string().default(SELF_PACKAGE_NAME),
474
+ selfPackageGiteeUrl: z.string().default(DEFAULT_SELF_PACKAGE_GITEE_URL),
475
+ selfPackageGithubUrl: z.string().default(DEFAULT_SELF_PACKAGE_GITHUB_URL),
384
476
  });
385
477
 
386
478
  constructor(ctx, config) {
@@ -390,14 +482,206 @@ class PluginDiscoveryService extends TypertRemoteService {
390
482
  this.catalog = pickNewerCatalog(loadBundledCatalog(), loadCachedCatalog());
391
483
  this.catalogCheckedAt = 0;
392
484
  this.catalogRefreshPromise = null;
485
+ this.selfUpdateCheckedAt = 0;
486
+ this.selfUpdateState = null;
487
+ this.selfUpdatePromise = null;
393
488
  this.ctx.effect(() => this.registerTools());
394
489
  // 启动后后台拉一次,不阻塞启动
395
490
  this.ctx.effect(() => {
396
491
  void this.refreshCatalog({ force: false }).catch(() => {});
492
+ void this.checkSelfUpdate({ force: false, auto: this.config.selfUpdateAuto === true }).catch(() => {});
397
493
  return () => {};
398
494
  });
399
495
  }
400
496
 
497
+ getLocalInstallMeta() {
498
+ const local = readLocalPackageMeta();
499
+ const real = (() => {
500
+ try {
501
+ return join(__dirname);
502
+ } catch {
503
+ return "";
504
+ }
505
+ })();
506
+ const isFileInstall = /[\\/]bundles[\\/]dsh-plugin-discovery$/i.test(real.replace(/\\/g, "/"));
507
+ return {
508
+ name: this.config.selfPackageName || local.name || SELF_PACKAGE_NAME,
509
+ version: local.version,
510
+ isFileInstall,
511
+ };
512
+ }
513
+
514
+ selfUpdateSourceUrls() {
515
+ const name = this.config.selfPackageName || SELF_PACKAGE_NAME;
516
+ const urls = [
517
+ {
518
+ kind: "npm",
519
+ label: "npm",
520
+ url: `https://registry.npmjs.org/${encodeURIComponent(name)}/latest`,
521
+ },
522
+ ];
523
+ const gitee = String(this.config.selfPackageGiteeUrl || "").trim();
524
+ const github = String(this.config.selfPackageGithubUrl || "").trim();
525
+ if (gitee) urls.push({ kind: "json", label: "gitee", url: gitee });
526
+ if (github) urls.push({ kind: "json", label: "github", url: github });
527
+ return urls;
528
+ }
529
+
530
+ async resolveInstalledSelfVersion() {
531
+ const local = this.getLocalInstallMeta();
532
+ try {
533
+ const bundles = await this.ctx.pluginManager.listBundles();
534
+ const row = (Array.isArray(bundles) ? bundles : []).find((item) => item && item.name === local.name);
535
+ if (row?.version) {
536
+ return { currentVersion: String(row.version), source: "bundle", isFileInstall: local.isFileInstall, name: local.name };
537
+ }
538
+ } catch {
539
+ /* ignore */
540
+ }
541
+ return {
542
+ currentVersion: local.version,
543
+ source: "package",
544
+ isFileInstall: local.isFileInstall,
545
+ name: local.name,
546
+ };
547
+ }
548
+
549
+ async checkSelfUpdate(options = {}) {
550
+ if (!this.config.enabled) {
551
+ return { ok: false, message: "发现插件已停用" };
552
+ }
553
+ if (this.config.selfUpdateCheckEnabled === false) {
554
+ return { ok: true, skipped: true, reason: "check disabled", update: this.selfUpdateState };
555
+ }
556
+ const force = options.force === true;
557
+ const auto = options.auto === true;
558
+ const ttl = this.config.selfUpdateCheckTtlMs ?? 21600000;
559
+ if (!force && this.selfUpdateCheckedAt && Date.now() - this.selfUpdateCheckedAt < ttl && this.selfUpdateState) {
560
+ if (auto && this.selfUpdateState.updateAvailable && !this.selfUpdateState.isFileInstall) {
561
+ return this.updateSelf({ latestVersion: this.selfUpdateState.latestVersion });
562
+ }
563
+ return { ok: true, skipped: true, reason: "ttl", update: this.selfUpdateState };
564
+ }
565
+ if (this.selfUpdatePromise) return this.selfUpdatePromise;
566
+
567
+ this.selfUpdatePromise = (async () => {
568
+ const installed = await this.resolveInstalledSelfVersion();
569
+ const latestInfo = await fetchLatestSelfVersion(this.selfUpdateSourceUrls());
570
+ this.selfUpdateCheckedAt = Date.now();
571
+ if (!latestInfo.version) {
572
+ this.selfUpdateState = {
573
+ ...installed,
574
+ latestVersion: null,
575
+ updateAvailable: false,
576
+ origin: null,
577
+ checkedAt: this.selfUpdateCheckedAt,
578
+ error: latestInfo.error,
579
+ };
580
+ return { ok: false, message: latestInfo.error || "无法获取最新版本", update: this.selfUpdateState };
581
+ }
582
+ const updateAvailable = compareSemver(latestInfo.version, installed.currentVersion) > 0;
583
+ this.selfUpdateState = {
584
+ name: installed.name,
585
+ currentVersion: installed.currentVersion,
586
+ latestVersion: latestInfo.version,
587
+ updateAvailable,
588
+ origin: latestInfo.origin,
589
+ isFileInstall: installed.isFileInstall,
590
+ checkedAt: this.selfUpdateCheckedAt,
591
+ };
592
+ if (auto && updateAvailable && !installed.isFileInstall && this.config.selfUpdateAuto !== false) {
593
+ const updated = await this.updateSelf({ latestVersion: latestInfo.version });
594
+ return { ...updated, update: this.selfUpdateState };
595
+ }
596
+ return {
597
+ ok: true,
598
+ update: this.selfUpdateState,
599
+ message: updateAvailable
600
+ ? `发现新版本 ${latestInfo.version}(当前 ${installed.currentVersion})`
601
+ : `已是最新版本 ${installed.currentVersion}`,
602
+ };
603
+ })().finally(() => {
604
+ this.selfUpdatePromise = null;
605
+ });
606
+
607
+ return this.selfUpdatePromise;
608
+ }
609
+
610
+ async updateSelf(options = {}) {
611
+ if (!this.config.enabled) {
612
+ return { ok: false, message: "发现插件已停用" };
613
+ }
614
+ const installed = await this.resolveInstalledSelfVersion();
615
+ if (installed.isFileInstall) {
616
+ return {
617
+ ok: false,
618
+ skipped: true,
619
+ message: "当前为本地 file: 开发安装,已跳过自动更新。发布版用户会从 npm 升级。",
620
+ update: this.selfUpdateState,
621
+ };
622
+ }
623
+ let latestVersion = typeof options.latestVersion === "string" ? options.latestVersion.trim() : "";
624
+ if (!latestVersion) {
625
+ const checked = await this.checkSelfUpdate({ force: true, auto: false });
626
+ latestVersion = checked?.update?.latestVersion || "";
627
+ if (!checked?.update?.updateAvailable) {
628
+ return {
629
+ ok: true,
630
+ updated: false,
631
+ message: checked?.message || "没有可更新版本",
632
+ update: checked?.update || this.selfUpdateState,
633
+ };
634
+ }
635
+ }
636
+ const name = installed.name || SELF_PACKAGE_NAME;
637
+ const spec = `${name}@${latestVersion}`;
638
+ const requestId =
639
+ globalThis.crypto?.randomUUID?.() || `pd-self-${Date.now()}-${Math.random().toString(16).slice(2)}`;
640
+ try {
641
+ const result = await this.ctx.pluginManager.installBundle(spec, {
642
+ requestId,
643
+ enabled: true,
644
+ });
645
+ const application = result?.application;
646
+ const failed = application === "failed" || application === "cancelled";
647
+ if (!failed) {
648
+ this.selfUpdateState = {
649
+ ...(this.selfUpdateState || {}),
650
+ name,
651
+ currentVersion: latestVersion,
652
+ latestVersion,
653
+ updateAvailable: false,
654
+ origin: this.selfUpdateState?.origin || "npm",
655
+ isFileInstall: false,
656
+ checkedAt: Date.now(),
657
+ };
658
+ }
659
+ return {
660
+ ok: !failed,
661
+ updated: !failed,
662
+ spec,
663
+ requestId,
664
+ application,
665
+ packageName: name,
666
+ version: latestVersion,
667
+ message: failed
668
+ ? result?.error?.message || `更新失败(${application || "unknown"})`
669
+ : `已更新到 ${latestVersion},请刷新页面或重启 dsh web 使界面生效`,
670
+ update: this.selfUpdateState,
671
+ result,
672
+ };
673
+ } catch (error) {
674
+ return {
675
+ ok: false,
676
+ updated: false,
677
+ spec,
678
+ requestId,
679
+ message: error instanceof Error ? error.message : String(error),
680
+ update: this.selfUpdateState,
681
+ };
682
+ }
683
+ }
684
+
401
685
  getCatalog() {
402
686
  return this.catalog || loadBundledCatalog();
403
687
  }
@@ -573,6 +857,14 @@ class PluginDiscoveryService extends TypertRemoteService {
573
857
 
574
858
  // 打开推荐/分类时尝试按 TTL 拉远程;失败静默回退本地
575
859
  await this.refreshCatalog({ force: opts.refresh === true }).catch(() => {});
860
+ const selfUpdate = await this.checkSelfUpdate({
861
+ force: opts.refresh === true,
862
+ auto: this.config.selfUpdateAuto === true,
863
+ }).catch((error) => ({
864
+ ok: false,
865
+ message: error instanceof Error ? error.message : String(error),
866
+ update: this.selfUpdateState,
867
+ }));
576
868
 
577
869
  const catalog = this.getCatalog();
578
870
  const personalize = mode === "recommend";
@@ -594,6 +886,9 @@ class PluginDiscoveryService extends TypertRemoteService {
594
886
  categories: catalog.categories,
595
887
  results,
596
888
  total: results.length,
889
+ selfUpdate: selfUpdate?.update || this.selfUpdateState,
890
+ selfUpdateMessage: selfUpdate?.message || null,
891
+ selfUpdated: selfUpdate?.updated === true,
597
892
  };
598
893
  }
599
894
 
@@ -929,6 +1224,28 @@ class PluginDiscoveryService extends TypertRemoteService {
929
1224
  }),
930
1225
  ),
931
1226
  );
1227
+ disposers.push(
1228
+ this.ctx.tools.register(
1229
+ defineTool({
1230
+ name: "plugin_discovery_check_update",
1231
+ description: "Check whether dsh-plugin-discovery has a newer npm/Gitee version and optionally update.",
1232
+ parameters: {
1233
+ autoUpdate: {
1234
+ type: "boolean",
1235
+ description: "If true and a newer version exists, install it now",
1236
+ },
1237
+ },
1238
+ output: toolOutput,
1239
+ async execute(args) {
1240
+ return service.checkSelfUpdate({
1241
+ force: true,
1242
+ auto: args.autoUpdate === true,
1243
+ });
1244
+ },
1245
+ presentCall: () => present("Check dsh-plugin-discovery update"),
1246
+ }),
1247
+ ),
1248
+ );
932
1249
  return () => {
933
1250
  for (const dispose of disposers.reverse()) dispose();
934
1251
  };
@@ -939,6 +1256,8 @@ class PluginDiscoveryService extends TypertRemoteService {
939
1256
 
940
1257
  markRemoteMethod(PluginDiscoveryService.prototype, "search");
941
1258
  markRemoteMethod(PluginDiscoveryService.prototype, "listCatalog");
1259
+ markRemoteMethod(PluginDiscoveryService.prototype, "checkSelfUpdate");
1260
+ markRemoteMethod(PluginDiscoveryService.prototype, "updateSelf");
942
1261
  markRemoteMethod(PluginDiscoveryService.prototype, "resolve");
943
1262
  markRemoteMethod(PluginDiscoveryService.prototype, "installPlugin");
944
1263
  markRemoteMethod(PluginDiscoveryService.prototype, "listInstalled");
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-plugin-discovery",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
- "description": "Discover and install community DSH bundles from the sidebar: newbie essentials, personalized recommendations, categories, and npm/GitHub search.",
5
+ "description": "Discover and install community DSH bundles from the sidebar: newbie essentials, recommendations, categories, self-update, and npm/GitHub search.",
6
6
  "author": "hjhcos",
7
7
  "license": "MIT",
8
8
  "keywords": [