dsh-plugin-discovery 1.0.0 → 1.1.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/client.js CHANGED
@@ -39,6 +39,7 @@ window.__ModuleLoader__.load({
39
39
  pageStatus: "第 {page} / {pages} 页 · 共 {total} 项",
40
40
  sourceNpmBadge: "npm",
41
41
  sourceGithubBadge: "GitHub",
42
+ sourceCatalogBadge: "精选",
42
43
  stars: "{n} stars",
43
44
  emptyTitle: "还没有列表",
44
45
  loadFailed: "加载失败",
@@ -48,6 +49,33 @@ window.__ModuleLoader__.load({
48
49
  installLog: "安装输出:{text}",
49
50
  installFailed: "安装失败",
50
51
  retry: "重试",
52
+ browseRecommend: "为你推荐",
53
+ browseCategory: "分类浏览",
54
+ browseSearch: "在线搜索",
55
+ recommendCardTitle: "个性化推荐",
56
+ recommendCardDesc: "根据你已安装的插件偏好,从精选目录排序推荐;目录可从 Gitee 自动更新。",
57
+ categoryCardTitle: "按分类浏览",
58
+ categoryCardDesc: "市面常用 DSH 插件收录在 JSON 目录中,优先从 Gitee 拉取最新版。",
59
+ categoryAll: "全部精选",
60
+ catalogMeta: "目录 v{version} · 来源 {origin}",
61
+ catalogOriginBundled: "插件内置",
62
+ catalogOriginCache: "本地缓存",
63
+ catalogOriginGitee: "Gitee",
64
+ catalogOriginGithub: "GitHub",
65
+ catalogOriginRemote: "远程",
66
+ refreshCatalog: "刷新目录",
67
+ reasonFeatured: "精选",
68
+ reasonRelated: "与已装相关",
69
+ reasonPopular: "热门",
70
+ reasonPet: "桌宠爱好者",
71
+ reasonTheme: "主题爱好者",
72
+ reasonIm: "IM 通道",
73
+ reasonMemory: "记忆相关",
74
+ reasonProductivity: "效率相关",
75
+ reasonMedia: "多媒体",
76
+ reasonRemote: "远程相关",
77
+ reasonMarketplace: "市场相关",
78
+ reasonAgent: "智能体相关",
51
79
  };
52
80
  const en = {
53
81
  panel: "Discover",
@@ -80,6 +108,7 @@ window.__ModuleLoader__.load({
80
108
  pageStatus: "Page {page} / {pages} · {total} items",
81
109
  sourceNpmBadge: "npm",
82
110
  sourceGithubBadge: "GitHub",
111
+ sourceCatalogBadge: "Curated",
83
112
  stars: "{n} stars",
84
113
  emptyTitle: "Nothing here yet",
85
114
  loadFailed: "Failed to load",
@@ -89,6 +118,33 @@ window.__ModuleLoader__.load({
89
118
  installLog: "Output: {text}",
90
119
  installFailed: "Install failed",
91
120
  retry: "Retry",
121
+ browseRecommend: "For you",
122
+ browseCategory: "Categories",
123
+ browseSearch: "Online search",
124
+ recommendCardTitle: "Personalized picks",
125
+ recommendCardDesc: "Ranked from the curated catalog; auto-updates from Gitee when possible.",
126
+ categoryCardTitle: "Browse by category",
127
+ categoryCardDesc: "Common DSH plugins live in a JSON catalog, preferring Gitee for updates.",
128
+ categoryAll: "All featured",
129
+ catalogMeta: "Catalog v{version} · {origin}",
130
+ catalogOriginBundled: "bundled",
131
+ catalogOriginCache: "local cache",
132
+ catalogOriginGitee: "Gitee",
133
+ catalogOriginGithub: "GitHub",
134
+ catalogOriginRemote: "remote",
135
+ refreshCatalog: "Refresh catalog",
136
+ reasonFeatured: "Featured",
137
+ reasonRelated: "Related to installed",
138
+ reasonPopular: "Popular",
139
+ reasonPet: "Pets fans",
140
+ reasonTheme: "Theme fans",
141
+ reasonIm: "Messaging",
142
+ reasonMemory: "Memory",
143
+ reasonProductivity: "Productivity",
144
+ reasonMedia: "Media",
145
+ reasonRemote: "Remote",
146
+ reasonMarketplace: "Marketplace",
147
+ reasonAgent: "Agents",
92
148
  };
93
149
 
94
150
  function format(template, vars) {
@@ -287,6 +343,38 @@ window.__ModuleLoader__.load({
287
343
  gap: "10px",
288
344
  alignItems: "center",
289
345
  },
346
+ browseTabs: {
347
+ display: "flex",
348
+ flexWrap: "wrap",
349
+ gap: "8px",
350
+ alignItems: "center",
351
+ },
352
+ chips: {
353
+ display: "flex",
354
+ flexWrap: "wrap",
355
+ gap: "8px",
356
+ alignItems: "center",
357
+ },
358
+ chip: {
359
+ display: "inline-flex",
360
+ alignItems: "center",
361
+ height: "28px",
362
+ padding: "0 12px",
363
+ borderRadius: "var(--dsw-radius-sm, 8px)",
364
+ border: "1px solid var(--dsw-alias-border-l2, #d8dde6)",
365
+ background: "var(--dsw-alias-bg-base, #ffffff)",
366
+ color: "var(--dsw-alias-label-secondary)",
367
+ fontSize: "12px",
368
+ fontWeight: 500,
369
+ lineHeight: "18px",
370
+ cursor: "pointer",
371
+ fontFamily: "var(--dsw-font-family)",
372
+ },
373
+ chipActive: {
374
+ borderColor: "var(--dsw-alias-state-business-primary, #4176e6)",
375
+ background: "color-mix(in srgb, var(--dsw-alias-state-business-primary, #4176e6) 12%, transparent)",
376
+ color: "var(--dsw-alias-state-business-primary, #4176e6)",
377
+ },
290
378
  searchGrow: {
291
379
  flex: "1 1 240px",
292
380
  minWidth: "200px",
@@ -575,6 +663,45 @@ window.__ModuleLoader__.load({
575
663
  );
576
664
  }
577
665
 
666
+ function Chip(props) {
667
+ return h(
668
+ "button",
669
+ {
670
+ type: "button",
671
+ disabled: props.disabled,
672
+ style: Object.assign({}, styles.chip, props.active ? styles.chipActive : null),
673
+ onClick: props.onClick,
674
+ },
675
+ props.children,
676
+ );
677
+ }
678
+
679
+ function reasonLabel(t, reason) {
680
+ const keyMap = {
681
+ featured: "reasonFeatured",
682
+ related: "reasonRelated",
683
+ popular: "reasonPopular",
684
+ pet: "reasonPet",
685
+ theme: "reasonTheme",
686
+ im: "reasonIm",
687
+ memory: "reasonMemory",
688
+ productivity: "reasonProductivity",
689
+ media: "reasonMedia",
690
+ remote: "reasonRemote",
691
+ marketplace: "reasonMarketplace",
692
+ agent: "reasonAgent",
693
+ };
694
+ const key = keyMap[reason] || "reasonPopular";
695
+ return t(key);
696
+ }
697
+
698
+ function categoryLabel(cat, lang) {
699
+ if (!cat) return "";
700
+ const label = cat.label || {};
701
+ if (lang === "en") return label.en || label.zh || cat.id;
702
+ return label.zh || label.en || cat.id;
703
+ }
704
+
578
705
  function unwrapRemote(result, fallbackMessage) {
579
706
  if (!result) return { ok: false, message: fallbackMessage };
580
707
  if (result.ok === false) {
@@ -590,6 +717,14 @@ window.__ModuleLoader__.load({
590
717
  function Panel(props) {
591
718
  const api = props.api;
592
719
  const t = props.t;
720
+ const lang = React.useMemo(() => {
721
+ const sample = String(t("browseRecommend") || "");
722
+ return /[\u4e00-\u9fff]/.test(sample) ? "zh" : "en";
723
+ }, [t]);
724
+ const [browseMode, setBrowseMode] = React.useState("recommend");
725
+ const [category, setCategory] = React.useState("beginner");
726
+ const [categories, setCategories] = React.useState([]);
727
+ const [catalogInfo, setCatalogInfo] = React.useState({ version: 0, origin: "bundled" });
593
728
  const [query, setQuery] = React.useState(DEFAULT_QUERY);
594
729
  const [source, setSource] = React.useState("all");
595
730
  const [results, setResults] = React.useState([]);
@@ -602,10 +737,14 @@ window.__ModuleLoader__.load({
602
737
  const [jobs, setJobs] = React.useState(() => readJobCache());
603
738
  const [page, setPage] = React.useState(1);
604
739
  const jobsRef = React.useRef(jobs);
740
+ const installedNamesRef = React.useRef(installedNames);
605
741
  React.useEffect(() => {
606
742
  jobsRef.current = jobs;
607
743
  writeJobCache(jobs);
608
744
  }, [jobs]);
745
+ React.useEffect(() => {
746
+ installedNamesRef.current = installedNames;
747
+ }, [installedNames]);
609
748
 
610
749
  const refreshInstalled = React.useCallback(async () => {
611
750
  if (!api || typeof api.listInstalled !== "function") return;
@@ -696,6 +835,52 @@ window.__ModuleLoader__.load({
696
835
  [markInstalled],
697
836
  );
698
837
 
838
+ const runCatalog = React.useCallback(
839
+ async (mode, nextCategory) => {
840
+ setBusy(true);
841
+ setMessage("");
842
+ setError("");
843
+ try {
844
+ if (!api || typeof api.listCatalog !== "function") {
845
+ setResults([]);
846
+ setError(t("loadFailed") + ": catalog unavailable");
847
+ return;
848
+ }
849
+ const [res] = await Promise.all([
850
+ api.listCatalog({
851
+ mode: mode === "category" ? "category" : "recommend",
852
+ category: nextCategory || "featured",
853
+ installedNames: [...installedNamesRef.current],
854
+ refresh: false,
855
+ }),
856
+ refreshInstalled(),
857
+ ]);
858
+ if (!res || !res.ok) {
859
+ setResults([]);
860
+ setError((res && res.message) || t("loadFailed"));
861
+ } else {
862
+ if (Array.isArray(res.categories) && res.categories.length) {
863
+ setCategories(res.categories);
864
+ }
865
+ setCatalogInfo({
866
+ version: res.catalogVersion || 0,
867
+ origin: res.catalogOrigin || "bundled",
868
+ });
869
+ const list = res.results || [];
870
+ setResults(list);
871
+ setPage(1);
872
+ if (!list.length) setMessage(t("noResults"));
873
+ }
874
+ } catch (e) {
875
+ setResults([]);
876
+ setError((e && e.message) || String(e));
877
+ } finally {
878
+ setBusy(false);
879
+ }
880
+ },
881
+ [api, refreshInstalled, t],
882
+ );
883
+
699
884
  const runSearch = React.useCallback(
700
885
  async (nextQuery, nextSource) => {
701
886
  setBusy(true);
@@ -834,23 +1019,40 @@ window.__ModuleLoader__.load({
834
1019
  }, [api, finishJob, refreshInstalled, t]);
835
1020
 
836
1021
  React.useEffect(() => {
1022
+ if (browseMode === "recommend") {
1023
+ void runCatalog("recommend", "featured");
1024
+ } else if (browseMode === "category") {
1025
+ void runCatalog("category", category);
1026
+ }
1027
+ }, [browseMode, category, runCatalog]);
1028
+
1029
+ React.useEffect(() => {
1030
+ if (browseMode !== "search") return;
837
1031
  void runSearch(query.trim() || DEFAULT_QUERY, source);
838
- }, [source, runSearch]);
1032
+ }, [browseMode, source, runSearch]);
839
1033
 
840
1034
  const pages = Math.max(1, Math.ceil(results.length / PAGE_SIZE));
841
1035
  const safePage = Math.min(page, pages);
842
1036
  const pageItems = results.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE);
843
1037
 
844
1038
  const onSearch = () => {
1039
+ setBrowseMode("search");
845
1040
  void runSearch(query.trim() || DEFAULT_QUERY, source);
846
1041
  };
847
1042
 
1043
+ const onBrowseMode = (mode) => {
1044
+ setBrowseMode(mode);
1045
+ setMessage("");
1046
+ setError("");
1047
+ };
1048
+
848
1049
  const onResolve = async () => {
849
1050
  const raw = resolveInput.trim();
850
1051
  // 关键词误填到解析框时,自动走搜索
851
1052
  if (raw && !/^https?:\/\//i.test(raw) && !/^(github:|gitlab:|bitbucket:)/i.test(raw) && !/^[@a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(raw) && (/\s/.test(raw) || /[\u4e00-\u9fff]/.test(raw))) {
852
1053
  setQuery(raw);
853
1054
  setResolveInput("");
1055
+ setBrowseMode("search");
854
1056
  void runSearch(raw, source);
855
1057
  return;
856
1058
  }
@@ -975,6 +1177,66 @@ window.__ModuleLoader__.load({
975
1177
  { value: "npm", label: t("sourceNpm") },
976
1178
  { value: "github", label: t("sourceGithub") },
977
1179
  ];
1180
+ const browseOptions = [
1181
+ { value: "recommend", label: t("browseRecommend") },
1182
+ { value: "category", label: t("browseCategory") },
1183
+ { value: "search", label: t("browseSearch") },
1184
+ ];
1185
+ const categoryChips = [
1186
+ { id: "beginner", label: categoryLabel(categories.find((c) => c.id === "beginner"), lang) || (lang === "en" ? "Newbie Essentials" : "新手必装") },
1187
+ { id: "featured", label: t("categoryAll") },
1188
+ ...categories
1189
+ .filter((cat) => cat && cat.id && cat.id !== "featured" && cat.id !== "beginner")
1190
+ .map((cat) => ({ id: cat.id, label: categoryLabel(cat, lang) })),
1191
+ ];
1192
+ const browseCardTitle =
1193
+ browseMode === "recommend"
1194
+ ? t("recommendCardTitle")
1195
+ : browseMode === "category"
1196
+ ? t("categoryCardTitle")
1197
+ : t("searchCardTitle");
1198
+ const browseCardDesc =
1199
+ browseMode === "recommend"
1200
+ ? t("recommendCardDesc")
1201
+ : browseMode === "category"
1202
+ ? t("categoryCardDesc")
1203
+ : t("searchCardDesc");
1204
+ const originKey =
1205
+ catalogInfo.origin === "gitee"
1206
+ ? "catalogOriginGitee"
1207
+ : catalogInfo.origin === "github"
1208
+ ? "catalogOriginGithub"
1209
+ : catalogInfo.origin === "cache"
1210
+ ? "catalogOriginCache"
1211
+ : catalogInfo.origin === "remote"
1212
+ ? "catalogOriginRemote"
1213
+ : "catalogOriginBundled";
1214
+ const onRefreshCatalog = () => {
1215
+ void (async () => {
1216
+ setBusy(true);
1217
+ try {
1218
+ if (api && typeof api.listCatalog === "function") {
1219
+ const res = await api.listCatalog({
1220
+ mode: browseMode === "category" ? "category" : "recommend",
1221
+ category,
1222
+ installedNames: [...installedNamesRef.current],
1223
+ refresh: true,
1224
+ });
1225
+ if (res && res.ok) {
1226
+ if (Array.isArray(res.categories)) setCategories(res.categories);
1227
+ setCatalogInfo({
1228
+ version: res.catalogVersion || 0,
1229
+ origin: res.catalogOrigin || "bundled",
1230
+ });
1231
+ setResults(res.results || []);
1232
+ setPage(1);
1233
+ }
1234
+ }
1235
+ } finally {
1236
+ setBusy(false);
1237
+ }
1238
+ })();
1239
+ };
978
1240
 
979
1241
  return h(
980
1242
  "div",
@@ -994,33 +1256,85 @@ window.__ModuleLoader__.load({
994
1256
  h(
995
1257
  "div",
996
1258
  { style: styles.cardHead },
997
- h("h2", { style: styles.cardTitle }, t("searchCardTitle")),
998
- h("p", { style: styles.cardDesc }, t("searchCardDesc")),
1259
+ h("h2", { style: styles.cardTitle }, browseCardTitle),
1260
+ h("p", { style: styles.cardDesc }, browseCardDesc),
1261
+ browseMode !== "search"
1262
+ ? h(
1263
+ "p",
1264
+ { style: styles.cardDesc },
1265
+ format(t("catalogMeta"), {
1266
+ version: catalogInfo.version || 0,
1267
+ origin: t(originKey),
1268
+ }),
1269
+ )
1270
+ : null,
999
1271
  ),
1000
1272
  h(
1001
1273
  "div",
1002
- { style: styles.toolbar },
1003
- h(
1004
- "div",
1005
- { style: styles.searchGrow },
1006
- h(Field, {
1007
- value: query,
1008
- placeholder: t("searchPlaceholder"),
1009
- disabled: busy,
1010
- onChange: (e) => setQuery(e.target.value),
1011
- onKeyDown: (e) => {
1012
- if (e.key === "Enter") onSearch();
1013
- },
1014
- }),
1015
- ),
1274
+ { style: styles.browseTabs },
1016
1275
  h(SourceSeg, {
1017
- value: source,
1018
- options: sourceOptions,
1276
+ value: browseMode,
1277
+ options: browseOptions,
1019
1278
  disabled: busy,
1020
- onChange: (next) => setSource(next),
1279
+ onChange: onBrowseMode,
1021
1280
  }),
1022
- h(Btn, { disabled: busy, onClick: onSearch }, busy ? t("searching") : t("search")),
1281
+ browseMode !== "search"
1282
+ ? h(
1283
+ Btn,
1284
+ {
1285
+ variant: "outline",
1286
+ size: "sm",
1287
+ disabled: busy,
1288
+ onClick: onRefreshCatalog,
1289
+ },
1290
+ t("refreshCatalog"),
1291
+ )
1292
+ : null,
1023
1293
  ),
1294
+ browseMode === "category"
1295
+ ? h(
1296
+ "div",
1297
+ { style: styles.chips },
1298
+ categoryChips.map((chip) =>
1299
+ h(
1300
+ Chip,
1301
+ {
1302
+ key: chip.id,
1303
+ active: category === chip.id,
1304
+ disabled: busy,
1305
+ onClick: () => setCategory(chip.id),
1306
+ },
1307
+ chip.label,
1308
+ ),
1309
+ ),
1310
+ )
1311
+ : null,
1312
+ browseMode === "search"
1313
+ ? h(
1314
+ "div",
1315
+ { style: styles.toolbar },
1316
+ h(
1317
+ "div",
1318
+ { style: styles.searchGrow },
1319
+ h(Field, {
1320
+ value: query,
1321
+ placeholder: t("searchPlaceholder"),
1322
+ disabled: busy,
1323
+ onChange: (e) => setQuery(e.target.value),
1324
+ onKeyDown: (e) => {
1325
+ if (e.key === "Enter") onSearch();
1326
+ },
1327
+ }),
1328
+ ),
1329
+ h(SourceSeg, {
1330
+ value: source,
1331
+ options: sourceOptions,
1332
+ disabled: busy,
1333
+ onChange: (next) => setSource(next),
1334
+ }),
1335
+ h(Btn, { disabled: busy, onClick: onSearch }, busy ? t("searching") : t("search")),
1336
+ )
1337
+ : null,
1024
1338
  ),
1025
1339
  h(
1026
1340
  "div",
@@ -1092,14 +1406,19 @@ window.__ModuleLoader__.load({
1092
1406
  const sourceBadge =
1093
1407
  item.source === "github"
1094
1408
  ? t("sourceGithubBadge")
1095
- : item.source === "npm"
1096
- ? t("sourceNpmBadge")
1097
- : item.source || "";
1409
+ : item.catalogId || item.featured != null || item.reason
1410
+ ? t("sourceCatalogBadge")
1411
+ : item.source === "npm"
1412
+ ? t("sourceNpmBadge")
1413
+ : item.source || "";
1098
1414
  const statusBadge = already
1099
1415
  ? h("span", { style: styles.badgeInstalled }, t("installedBadge"))
1100
1416
  : installing
1101
1417
  ? h("span", { style: styles.badgeInstalling }, t("installingBadge"))
1102
1418
  : h("span", { style: styles.badge }, t("notInstalledBadge"));
1419
+ const whyBadge = item.reason
1420
+ ? h("span", { style: styles.badge }, reasonLabel(t, item.reason))
1421
+ : null;
1103
1422
  return h(
1104
1423
  "div",
1105
1424
  { key: spec, style: styles.row },
@@ -1112,6 +1431,7 @@ window.__ModuleLoader__.load({
1112
1431
  "div",
1113
1432
  { style: styles.meta },
1114
1433
  sourceBadge ? h("span", { style: styles.badge }, sourceBadge) : null,
1434
+ whyBadge,
1115
1435
  statusBadge,
1116
1436
  item.stars != null ? h("span", null, format(t("stars"), { n: item.stars })) : null,
1117
1437
  item.url
@@ -1195,6 +1515,8 @@ window.__ModuleLoader__.load({
1195
1515
  const QUERY_CODEC = strictJson("dsh-plugin-discovery#pluginDiscovery/search:query");
1196
1516
  const OPTIONS_CODEC = strictJson("dsh-plugin-discovery#pluginDiscovery/search:options");
1197
1517
  const SEARCH_RESULT = strictJson("dsh-plugin-discovery#pluginDiscovery/search:result");
1518
+ const CATALOG_OPTIONS_CODEC = strictJson("dsh-plugin-discovery#pluginDiscovery/listCatalog:options");
1519
+ const CATALOG_RESULT = strictJson("dsh-plugin-discovery#pluginDiscovery/listCatalog:result");
1198
1520
  const INPUT_CODEC = strictJson("dsh-plugin-discovery#pluginDiscovery/resolve:input");
1199
1521
  const RESOLVE_RESULT = strictJson("dsh-plugin-discovery#pluginDiscovery/resolve:result");
1200
1522
  const SPEC_CODEC = strictJson("dsh-plugin-discovery#pluginDiscovery/installPlugin:spec");
@@ -1225,6 +1547,23 @@ window.__ModuleLoader__.load({
1225
1547
  ],
1226
1548
  result: SEARCH_RESULT,
1227
1549
  },
1550
+ {
1551
+ id: "dsh-plugin-discovery#pluginDiscovery/listCatalog",
1552
+ service: "pluginDiscovery",
1553
+ namespace: "pluginDiscovery",
1554
+ method: "listCatalog",
1555
+ invocation: { kind: "direct" },
1556
+ parameters: [
1557
+ {
1558
+ name: "options",
1559
+ wire: "options",
1560
+ source: "json",
1561
+ acceptsUndefined: true,
1562
+ codec: CATALOG_OPTIONS_CODEC,
1563
+ },
1564
+ ],
1565
+ result: CATALOG_RESULT,
1566
+ },
1228
1567
  {
1229
1568
  id: "dsh-plugin-discovery#pluginDiscovery/resolve",
1230
1569
  service: "pluginDiscovery",
@@ -1304,6 +1643,20 @@ window.__ModuleLoader__.load({
1304
1643
  if (wire && wire.ok === false) return { ok: false, message: value.message, results: [] };
1305
1644
  return value;
1306
1645
  },
1646
+ listCatalog: async (opts) => {
1647
+ if (mountError) return { ok: false, message: mountError, categories: [], results: [] };
1648
+ const remote = remoteNs();
1649
+ if (!remote || typeof remote.listCatalog !== "function") {
1650
+ return { ok: false, message: "remote.pluginDiscovery.listCatalog unavailable", categories: [], results: [] };
1651
+ }
1652
+ const wire = await remote.listCatalog(opts || {});
1653
+ const value = unwrapRemote(wire, "listCatalog failed");
1654
+ if (value && value.ok === false && value.message && !value.results) return value;
1655
+ if (wire && wire.ok === false) {
1656
+ return { ok: false, message: value.message, categories: [], results: [] };
1657
+ }
1658
+ return value;
1659
+ },
1307
1660
  resolve: async (input) => {
1308
1661
  if (mountError) return { ok: false, message: mountError };
1309
1662
  const remote = remoteNs();
package/cordis.patch.yml CHANGED
@@ -8,3 +8,8 @@
8
8
  maxInspect: 10
9
9
  githubToken: ""
10
10
  openPanelOnLoad: false
11
+ # 推荐目录远程自动更新:优先 Gitee raw,失败再试 GitHub;按 JSON version 升级本地缓存
12
+ catalogRemoteEnabled: true
13
+ catalogGiteeUrl: 'https://gitee.com/hjhcos/dsh-plugin-discovery/raw/main/data/recommended.json'
14
+ catalogGithubUrl: 'https://raw.githubusercontent.com/hjhcos/dsh-plugin-discovery/main/data/recommended.json'
15
+ catalogRefreshTtlMs: 3600000