dsh-plugin 0.1.1 → 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.
Files changed (67) hide show
  1. package/README.en.md +56 -28
  2. package/README.md +56 -28
  3. package/client/client.js +2282 -230
  4. package/client/client.js.map +1 -1
  5. package/lib/http/routes.js +432 -0
  6. package/lib/index.js +17 -8
  7. package/lib/services/install-types.js +11 -0
  8. package/lib/services/install.js +4 -0
  9. package/lib/services/installed-versions.js +66 -0
  10. package/lib/services/loader.js +98 -0
  11. package/lib/services/npm-resolve.js +67 -0
  12. package/lib/services/pending-restart.js +34 -0
  13. package/lib/services/preflight.js +94 -0
  14. package/lib/services/profile.js +88 -0
  15. package/lib/services/progress.js +50 -0
  16. package/lib/services/task-queue.js +457 -0
  17. package/lib/types/http/routes.d.ts +23 -0
  18. package/lib/types/index.d.ts +27 -0
  19. package/lib/types/services/install-types.d.ts +69 -0
  20. package/lib/types/services/install.d.ts +18 -0
  21. package/lib/types/services/installed-versions.d.ts +24 -0
  22. package/lib/types/services/loader.d.ts +43 -0
  23. package/lib/types/services/npm-resolve.d.ts +6 -0
  24. package/lib/types/services/pending-restart.d.ts +13 -0
  25. package/lib/types/services/preflight.d.ts +6 -0
  26. package/lib/types/services/profile.d.ts +24 -0
  27. package/lib/types/services/progress.d.ts +17 -0
  28. package/lib/types/services/task-queue.d.ts +82 -0
  29. package/package.json +27 -6
  30. package/src/client/components/CatalogControls.tsx +104 -0
  31. package/src/client/components/CatalogHeader.tsx +78 -0
  32. package/src/client/components/CatalogList.tsx +75 -0
  33. package/src/client/components/CategoryTabs.tsx +35 -0
  34. package/src/client/components/Dropdown.tsx +80 -0
  35. package/src/client/components/NotificationsModal.tsx +243 -0
  36. package/src/client/components/PluginCard.tsx +104 -0
  37. package/src/client/components/PluginHubSection.tsx +342 -0
  38. package/src/client/components/ProgressView.tsx +23 -0
  39. package/src/client/components/icons.tsx +125 -0
  40. package/src/client/components/modals.tsx +368 -0
  41. package/src/client/hooks/useCatalog.ts +202 -0
  42. package/src/client/hooks/useTaskQueue.ts +502 -0
  43. package/src/client/index.tsx +8 -422
  44. package/src/client/lib/catalog.ts +240 -0
  45. package/src/client/lib/env.ts +16 -0
  46. package/src/client/lib/failures.ts +165 -0
  47. package/src/client/lib/format.ts +22 -0
  48. package/src/client/locales.ts +188 -18
  49. package/src/client/styles/Dropdown.module.css +123 -0
  50. package/src/client/styles/Header.module.css +432 -0
  51. package/src/client/styles/List.module.css +324 -0
  52. package/src/client/styles/Modal.module.css +1006 -0
  53. package/src/client/styles/tokens.module.css +91 -0
  54. package/src/client/types.ts +80 -0
  55. package/src/server/http/routes.ts +432 -0
  56. package/src/server/index.ts +35 -0
  57. package/src/server/services/install-types.ts +69 -0
  58. package/src/server/services/install.ts +18 -0
  59. package/src/server/services/installed-versions.ts +84 -0
  60. package/src/server/services/loader.ts +110 -0
  61. package/src/server/services/npm-resolve.ts +67 -0
  62. package/src/server/services/pending-restart.ts +36 -0
  63. package/src/server/services/preflight.ts +97 -0
  64. package/src/server/services/profile.ts +89 -0
  65. package/src/server/services/progress.ts +48 -0
  66. package/src/server/services/task-queue.ts +492 -0
  67. package/src/client/Section.module.css +0 -589
package/client/client.js CHANGED
@@ -5,93 +5,100 @@ window.__ModuleLoader__.load({ id: "dsh-plugin", factory: (require) => {
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
- //#region \0dsh-css:src/client/Section.module.css.mjs
9
- const css = ".XlLbKa_root{min-width:0;height:100%;color:var(--dsw-alias-label-primary,#1f2328);flex-direction:column;gap:8px;display:flex}.XlLbKa_header{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;padding:2px 2px 0;display:flex}.XlLbKa_brand{align-items:center;gap:10px;min-width:0;display:flex}.XlLbKa_brandText{flex-direction:column;gap:1px;min-width:0;display:flex}.XlLbKa_title{margin:0;font-size:14px;font-weight:600;line-height:20px}.XlLbKa_tagline{color:var(--dsw-alias-label-tertiary,#8b93a1);-webkit-line-clamp:2;-webkit-box-orient:vertical;margin:0;font-size:12px;line-height:18px;display:-webkit-box;overflow:hidden}.XlLbKa_controls{flex-shrink:0;align-items:center;gap:8px;display:flex}.XlLbKa_adBanner{color:#fff;cursor:pointer;-webkit-user-select:none;user-select:none;background:linear-gradient(90deg,#4f46e5 0%,#7c3aed 100%);border:1px solid #8b5cf68c;border-radius:8px;align-items:center;gap:8px;padding:7px 12px;text-decoration:none;transition:filter .15s,box-shadow .15s;display:flex;box-shadow:0 2px 10px #4f46e547}.XlLbKa_adBanner:hover{filter:brightness(1.1);box-shadow:0 4px 16px #7c3aed6b}.XlLbKa_adBadge{letter-spacing:.04em;color:#fff;white-space:nowrap;background:#ffffff29;border:1px solid #ffffff8c;border-radius:4px;flex-shrink:0;padding:3px 6px;font-size:10px;font-weight:700;line-height:1}.XlLbKa_adText{text-overflow:ellipsis;white-space:nowrap;color:#fff;min-width:0;font-size:12px;line-height:18px;overflow:hidden}.XlLbKa_adArrow{color:#fff;flex-shrink:0;margin-left:auto;font-size:13px}.XlLbKa_search{width:190px;color:inherit;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:0 0;border-radius:6px;outline:none;padding:4px 9px;font-size:12px;line-height:18px;transition:border-color .12s}.XlLbKa_search::placeholder{color:var(--dsw-alias-label-tertiary,#8b93a1)}.XlLbKa_search:focus{border-color:var(--dsw-alias-brand-primary,#4f6ef7)}.XlLbKa_sort{color:var(--dsw-alias-label-secondary,#6b7280);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);cursor:pointer;-webkit-user-select:none;user-select:none;background:0 0;border-radius:6px;outline:none;padding:4px 8px;font-size:12px;line-height:18px}.XlLbKa_sort:hover{color:var(--dsw-alias-label-primary,#1f2328);background:var(--dsw-alias-interactive-bg-hover,#80808014);border-color:var(--dsw-alias-button-ghost-active-border,#d0d4da)}.XlLbKa_langBtn{color:var(--dsw-alias-label-secondary,#6b7280);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);cursor:pointer;-webkit-user-select:none;user-select:none;background:0 0;border-radius:6px;flex-shrink:0;padding:4px 9px;font-size:12px;line-height:18px;transition:color .12s,border-color .12s}.XlLbKa_langBtn:hover{color:var(--dsw-alias-label-primary,#1f2328);background:var(--dsw-alias-interactive-bg-hover,#80808014);border-color:var(--dsw-alias-button-ghost-active-border,#d0d4da)}.XlLbKa_openBtn{color:var(--dsw-alias-label-secondary,#6b7280);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none;background:0 0;border-radius:6px;flex-shrink:0;align-items:center;gap:4px;padding:3px 9px;font-size:12px;font-weight:500;line-height:20px;text-decoration:none;transition:color .12s,border-color .12s;display:inline-flex}.XlLbKa_openBtn:hover{color:var(--dsw-alias-label-primary,#1f2328);background:var(--dsw-alias-interactive-bg-hover,#80808014);border-color:var(--dsw-alias-button-ghost-active-border,#d0d4da)}.XlLbKa_openArrow{font-size:12px;line-height:1}.XlLbKa_tabs{border-bottom:1px solid var(--dsw-alias-border-l1,#eceef1);flex-wrap:wrap;align-items:center;gap:6px;padding:2px 2px 8px;display:flex}.XlLbKa_tab,.XlLbKa_tabActive{cursor:pointer;-webkit-user-select:none;user-select:none;border:1px solid #0000;border-radius:999px;flex-shrink:0;align-items:center;gap:6px;padding:3px 10px;font-size:12px;line-height:18px;transition:color .12s,background .12s,border-color .12s;display:inline-flex}.XlLbKa_tab{color:var(--dsw-alias-label-secondary,#6b7280);border-color:var(--dsw-alias-border-l2,#e5e7eb);background:0 0}.XlLbKa_tab:hover{color:var(--dsw-alias-label-primary,#1f2328);background:var(--dsw-alias-interactive-bg-hover,#80808014);border-color:var(--dsw-alias-border-l1,#eceef1)}.XlLbKa_tabActive{color:var(--dsw-alias-label-primary-foreground,#fff);background:var(--dsw-alias-button-primary-fill,#1f2328);border-color:var(--dsw-alias-button-primary-fill,#1f2328)}.XlLbKa_tabCount,.XlLbKa_tabCountActive{text-align:center;border-radius:999px;min-width:16px;padding:0 5px;font-size:10px;line-height:14px}.XlLbKa_tabCount{color:var(--dsw-alias-label-tertiary,#8b93a1);background:var(--dsw-alias-bg-layer-2,#8080801a)}.XlLbKa_tabActive .XlLbKa_tabCountActive{color:var(--dsw-alias-label-primary-foreground,#fff);background:#80808038}.XlLbKa_body{flex-direction:column;flex:1;min-height:0;display:flex}.XlLbKa_list{flex-direction:column;flex:1;gap:6px;min-height:0;padding:2px 4px 4px 2px;display:flex;overflow-y:auto}.XlLbKa_card{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-1,#8080800f);border-radius:8px;justify-content:space-between;align-items:stretch;gap:12px;padding:9px 12px;transition:border-color .12s,background .12s;display:flex}.XlLbKa_card:hover{border-color:var(--dsw-alias-brand-primary,#4f6ef7);background:var(--dsw-alias-bg-layer-2,#8080801a)}.XlLbKa_cardMain{flex-direction:column;gap:4px;min-width:0;display:flex}.XlLbKa_cardHead{align-items:center;gap:6px;min-width:0;display:flex}.XlLbKa_cardTitle{white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:600;line-height:18px;overflow:hidden}.XlLbKa_categoryBadge,.XlLbKa_verified{border:1px solid #0000;border-radius:4px;flex-shrink:0;padding:0 6px;font-size:10px;line-height:16px}.XlLbKa_categoryBadge{color:var(--dsw-alias-brand-primary,#4f6ef7);border-color:var(--dsw-alias-button-ghost-active-border,#4f6ef740)}.XlLbKa_verified{color:var(--dsw-alias-state-success-primary,#1a7f37);border-color:var(--dsw-alias-state-success-secondary,#1a7f374d)}.XlLbKa_desc{color:var(--dsw-alias-label-secondary,#6b7280);-webkit-line-clamp:2;-webkit-box-orient:vertical;margin:0;font-size:12px;line-height:18px;display:-webkit-box;overflow:hidden}.XlLbKa_topics{flex-wrap:wrap;align-items:center;gap:4px;min-width:0;display:flex}.XlLbKa_topic{color:var(--dsw-alias-label-tertiary,#8b93a1);background:var(--dsw-alias-bg-layer-2,#8080801a);white-space:nowrap;border-radius:4px;flex-shrink:0;padding:0 6px;font-size:10px;line-height:16px}.XlLbKa_cardSide{flex-direction:column;flex-shrink:0;justify-content:space-between;align-items:flex-end;gap:6px;display:flex}.XlLbKa_stats{flex-direction:column;align-items:flex-end;gap:1px;display:flex}.XlLbKa_star{color:var(--dsw-alias-state-warn-primary,#b8860b);white-space:nowrap;font-size:11px;line-height:16px}.XlLbKa_fork{color:var(--dsw-alias-label-tertiary,#8b93a1);white-space:nowrap;font-size:11px;line-height:16px}.XlLbKa_date{color:var(--dsw-alias-label-tertiary,#8b93a1);white-space:nowrap;font-size:10px;line-height:14px}.XlLbKa_installBtn,.XlLbKa_installBtnCopied,.XlLbKa_detailBtn{cursor:pointer;border-radius:6px;padding:2px 10px;font-size:11px;line-height:18px;transition:color .12s,border-color .12s,background .12s}.XlLbKa_installBtn{color:var(--dsw-alias-label-primary,#1f2328);background:var(--dsw-alias-bg-layer-2,#8080800f);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);-webkit-user-select:none;user-select:none}.XlLbKa_installBtn:hover{color:var(--dsw-alias-label-primary,#1f2328);background:var(--dsw-alias-interactive-bg-hover,#8080801f);border-color:var(--dsw-alias-button-ghost-active-border,#d0d4da)}.XlLbKa_detailBtn{color:var(--dsw-alias-label-primary-foreground,#fff);background:var(--dsw-alias-button-primary-fill,#1f2328);-webkit-user-select:none;user-select:none;border:1px solid #0000;align-items:center;font-weight:500;text-decoration:none;display:inline-flex}.XlLbKa_detailBtn:hover{background:var(--dsw-alias-button-primary-hover,#43454a)}.XlLbKa_actions{align-items:center;gap:6px;display:flex}.XlLbKa_installBtnCopied{color:var(--dsw-alias-state-success-primary,#1a7f37);border:1px solid var(--dsw-alias-state-success-secondary,#1a7f3766);-webkit-user-select:none;user-select:none;background:#22c55e24}.XlLbKa_toast{z-index:1000;background:var(--dsw-alias-button-primary-fill,#1f2328);color:var(--dsw-alias-label-primary-foreground,#fff);pointer-events:none;white-space:nowrap;border:1px solid #0000;border-radius:8px;padding:10px 16px;font-size:12px;font-weight:500;line-height:18px;animation:.22s ease-out XlLbKa_toastIn;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);box-shadow:0 6px 24px #00000047}@keyframes XlLbKa_toastIn{0%{opacity:0;transform:translate(-50%,-44%)}to{opacity:1;transform:translate(-50%,-50%)}}.XlLbKa_state{text-align:center;min-height:160px;color:var(--dsw-alias-label-tertiary,#8b93a1);flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;padding:24px;font-size:12px;line-height:18px;display:flex}.XlLbKa_stateTitle{color:var(--dsw-alias-label-primary,#1f2328);font-size:13px;font-weight:600}.XlLbKa_stateDesc{max-width:420px}.XlLbKa_retryBtn{color:var(--dsw-alias-label-secondary,#6b7280);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);cursor:pointer;-webkit-user-select:none;user-select:none;background:0 0;border-radius:6px;margin-top:4px;padding:4px 12px;font-size:12px;line-height:18px}.XlLbKa_retryBtn:hover{color:var(--dsw-alias-label-primary,#1f2328);background:var(--dsw-alias-interactive-bg-hover,#80808014);border-color:var(--dsw-alias-button-ghost-active-border,#d0d4da)}.XlLbKa_footer{border-top:1px solid var(--dsw-alias-border-l1,#eceef1);flex-shrink:0;justify-content:space-between;align-items:center;gap:8px;padding:6px 4px 0;display:flex}.XlLbKa_footText{color:var(--dsw-alias-label-tertiary,#8b93a1);font-size:11px;line-height:16px}.XlLbKa_footLink{color:var(--dsw-alias-brand-primary,#4f6ef7);white-space:nowrap;-webkit-user-select:none;user-select:none;font-size:11px;line-height:16px;text-decoration:none}.XlLbKa_footLink:hover{text-decoration:underline}";
10
- const tagId = "dsh-plugin/Section.module.css";
11
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
12
- const tag = document.createElement("style");
13
- tag.dataset.plugin = "dsh-plugin";
14
- tag.dataset.pluginCss = tagId;
15
- tag.textContent = css;
16
- document.head.appendChild(tag);
17
- }
18
- var Section_module_css_default = {
19
- "list": "XlLbKa_list",
20
- "cardSide": "XlLbKa_cardSide",
21
- "tabs": "XlLbKa_tabs",
22
- "openBtn": "XlLbKa_openBtn",
23
- "fork": "XlLbKa_fork",
24
- "installBtnCopied": "XlLbKa_installBtnCopied",
25
- "detailBtn": "XlLbKa_detailBtn",
26
- "adBanner": "XlLbKa_adBanner",
27
- "root": "XlLbKa_root",
28
- "header": "XlLbKa_header",
29
- "adText": "XlLbKa_adText",
30
- "tabActive": "XlLbKa_tabActive",
31
- "desc": "XlLbKa_desc",
32
- "actions": "XlLbKa_actions",
33
- "installBtn": "XlLbKa_installBtn",
34
- "stateDesc": "XlLbKa_stateDesc",
35
- "footer": "XlLbKa_footer",
36
- "cardMain": "XlLbKa_cardMain",
37
- "tabCountActive": "XlLbKa_tabCountActive",
38
- "cardTitle": "XlLbKa_cardTitle",
39
- "stats": "XlLbKa_stats",
40
- "retryBtn": "XlLbKa_retryBtn",
41
- "footText": "XlLbKa_footText",
42
- "card": "XlLbKa_card",
43
- "footLink": "XlLbKa_footLink",
44
- "brand": "XlLbKa_brand",
45
- "toastIn": "XlLbKa_toastIn",
46
- "adArrow": "XlLbKa_adArrow",
47
- "adBadge": "XlLbKa_adBadge",
48
- "search": "XlLbKa_search",
49
- "body": "XlLbKa_body",
50
- "date": "XlLbKa_date",
51
- "brandText": "XlLbKa_brandText",
52
- "toast": "XlLbKa_toast",
53
- "state": "XlLbKa_state",
54
- "tab": "XlLbKa_tab",
55
- "langBtn": "XlLbKa_langBtn",
56
- "cardHead": "XlLbKa_cardHead",
57
- "topics": "XlLbKa_topics",
58
- "stateTitle": "XlLbKa_stateTitle",
59
- "sort": "XlLbKa_sort",
60
- "controls": "XlLbKa_controls",
61
- "title": "XlLbKa_title",
62
- "tagline": "XlLbKa_tagline",
63
- "tabCount": "XlLbKa_tabCount",
64
- "categoryBadge": "XlLbKa_categoryBadge",
65
- "topic": "XlLbKa_topic",
66
- "star": "XlLbKa_star",
67
- "openArrow": "XlLbKa_openArrow",
68
- "verified": "XlLbKa_verified"
69
- };
70
- //#endregion
71
8
  //#region src/client/locales.ts
72
9
  /** zh/en dictionaries for the DSH-Plugin Hub settings section. */
73
10
  const zh = {
74
11
  nav: "插件中心",
75
- title: "DSH-Plugin Hub for DeepSeek Harness",
76
- tagline: "DSH plugin 插件中心:收录 {total} 个 DeepSeek Harness 插件,人工精选验证 {verified},每日更新 · 免费安装",
12
+ title: "DSH-Plugin Hub",
13
+ tagline: "DSH-Plugin 插件中心:面向 DeepSeek Harness 插件收录平台,人工审验、来源可溯,免费安装",
77
14
  adBadge: "推荐",
78
- ad: "DSH-Plugin 插件中心:收录 {total} 个 DeepSeek Harness 插件,其中 {verified} 人工精选验证,每日更新,免费按分类浏览、搜索、下载与安装,人工验证、来源可溯",
15
+ ad: "DSH-Plugin 插件中心:收录 {total} 款插件,{verified} 款人工精选验证,每日更新",
79
16
  search: "搜索插件名称、描述、标签…",
80
17
  all: "全部",
81
18
  sortStars: "Star 最多",
19
+ sortForks: "Fork 最多",
82
20
  sortUpdated: "最近更新",
83
21
  sortNewest: "最新收录",
84
- open: "在浏览器中打开",
85
22
  openHint: "打开 dsh-plugin.org",
86
- openBtn: "DSH-Plugin 官网",
87
- toEn: "Switch to English",
23
+ toggleLangHint: "切换界面语言",
88
24
  fork: "Fork",
89
25
  loading: "正在加载插件数据…",
90
26
  failed: "插件数据加载失败",
91
27
  failedDesc: "请稍后重试,或点击右上角按钮在浏览器中打开插件中心。",
92
28
  retry: "重试",
93
- copy: "复制安装命令",
29
+ install: "安装",
94
30
  toastCopied: "安装命令已复制,去 dsh 终端粘贴即可安装",
31
+ installed: "已安装",
32
+ notInstalled: "未安装",
33
+ filterAllHint: "显示全部插件",
34
+ filterInstalledHint: "筛选当前分类下的已安装插件",
35
+ filterNotInstalledHint: "筛选当前分类下的未安装插件",
36
+ filterInstalledNone: "当前分类下没有已安装插件",
37
+ filterNotInstalledNone: "当前分类下没有未安装插件",
38
+ uninstall: "卸载",
39
+ uninstalling: "卸载中…",
40
+ uninstallTitle: "确认卸载",
41
+ uninstallDesc: "将从当前环境卸载该插件。",
42
+ uninstallDone: "卸载成功",
43
+ uninstallFail: "卸载失败,请稍后重试",
44
+ uninstallResultTitle: "卸载完成",
45
+ uninstallResultDesc: "插件已从当前环境移除。",
46
+ confirmTitle: "确认安装",
47
+ confirmDesc: "安装前请前往插件仓库仔细阅读源码,确认可信后再安装",
48
+ confirmPlugin: "插件",
49
+ confirmSource: "来源",
50
+ confirmCommand: "安装命令",
51
+ copyInstallCommand: "复制安装命令",
52
+ copyCmdLabel: "复制",
53
+ copyUninstallCommand: "复制卸载命令",
54
+ installNow: "直接安装",
55
+ installing: "安装中…",
56
+ installDone: "安装成功",
57
+ installResultTitle: "插件安装成功",
58
+ installResultDesc: "",
59
+ confirmUpdateTitle: "确认更新",
60
+ updateNow: "直接更新",
61
+ updating: "更新中…",
62
+ queuedUpdateTitle: "已加入更新排队",
63
+ updateResultTitle: "更新完成",
64
+ updateResultDesc: "插件已更新到最新版本。",
65
+ installFail: "安装失败",
66
+ confirmCancel: "取消",
67
+ doneBtn: "完成",
68
+ restartNow: "立即重启",
69
+ restartLater: "稍后重启",
70
+ restarting: "正在重启…",
71
+ restartHint: "部分插件(如插件市场、顶层 bundle)需重启后才会生效。",
72
+ restartPendingHint: "安装成功,重启后生效",
73
+ restartPendingHintUninstall: "卸载成功,重启后移除",
74
+ sectionPendingRestart: "待重启",
75
+ sectionInProgress: "进行中",
76
+ queuedTitle: "已加入安装排队",
77
+ queuedUninstallTitle: "已加入卸载排队",
78
+ queuedHint: "已加入排队,前序任务完成后将自动开始,可关闭本窗口继续浏览。",
79
+ cancelTask: "取消任务",
80
+ cancelling: "正在取消…",
81
+ errorTitle: "操作失败",
82
+ errorTitleInstall: "安装失败",
83
+ errorTitleUninstall: "卸载失败",
84
+ errorPlugin: "插件",
85
+ errorClose: "知道了",
86
+ errorCopy: "复制错误信息",
87
+ errCopied: "错误信息已复制",
88
+ notifications: "通知",
89
+ notificationsBtn: "通知",
90
+ notificationsHint: "查看安装/卸载通知(含失败记录)",
91
+ notificationsDesc: "安装、卸载任务的成败都会记录在这里,即使错过提示也能随时回来查看。",
92
+ notificationsEmpty: "暂无通知记录",
93
+ notificationsClear: "清空通知",
94
+ removeNotification: "删除这条通知",
95
+ failCopy: "复制完整错误信息",
96
+ failIssueHint: "带着错误日志去作者仓库一键提交 Issue(正文含官网收录链接)",
97
+ failIssueBig: "一键提交 BUG 到 GitHub Issue 为开源作贡献",
98
+ failPrepareHint: "该插件安装时的构建脚本执行失败(git 分发常缺失子模块或构建产物),属插件打包分发问题。请向作者仓库提交 Issue 反馈。",
99
+ failPackagingHint: "该插件不支持官方默认安装方式:其 git 分发缺少构建产物(package.json 声明的入口文件在仓库中不存在),说明作者未适配官方安装流程。请到作者仓库提交 Issue 反馈,请其提交构建产物或发布 npm 版。",
100
+ failIgnoredBuild: "该插件依赖了需要编译的原生模块(如 node-pty),pnpm 默认拦截这类构建脚本导致安装失败 —— 其他插件不受影响,属该插件的依赖/打包问题。建议向作者仓库一键反馈,请作者改用预编译版本(如 node-pty-prebuilt-multiarch)。",
101
+ reportIssue: "去插件仓库提交 Issue",
95
102
  detail: "打开详情",
96
103
  noResult: "没有找到匹配的插件",
97
104
  noResultDesc: "换个关键词试试,或前往网站浏览全部插件。",
@@ -99,36 +106,111 @@ window.__ModuleLoader__.load({ id: "dsh-plugin", factory: (require) => {
99
106
  daysAgo: "更新于 {days} 天前",
100
107
  monthsAgo: "更新于 {months} 个月前",
101
108
  yearsAgo: "更新于 {years} 年前",
102
- pluginsTotal: "共 {n} 个插件",
103
- filteredCount: "匹配 {n} 个",
109
+ pluginsTotal: "结果 {n} 条插件",
110
+ filterResults: "结果 {n} 条插件",
104
111
  more: "还有 {n} 个插件,去网站查看全部",
105
112
  browseAll: "浏览全部 {n} 个插件",
106
113
  verified: "已验证",
114
+ version: "版本",
115
+ update: "可更新",
116
+ updateAvailable: "有更新",
117
+ updateAvailableHint: "检测到更新:有版本号的仓库比对版本号,无版本号的仓库比对仓库最近更新时间",
107
118
  empty: "该分类暂无插件",
108
119
  dataFrom: "数据源 dsh-plugin.org · 每日人工更新"
109
120
  };
110
121
  const en = {
111
122
  nav: "Plugin Hub",
112
- title: "DSH-Plugin Hub for DeepSeek Harness",
113
- tagline: "DSH plugin marketplace · {total} DeepSeek Harness plugins indexed · {verified} human-verified · updated daily · free to install",
123
+ title: "DSH-Plugin Hub",
124
+ tagline: "DSH-Plugin Hub: a human-verified, free marketplace for DeepSeek Harness plugins",
114
125
  adBadge: "Featured",
115
- ad: "DSH-Plugin Hub: {total} DeepSeek Harness plugins indexed, {verified} of which are human-verified. Updated daily — browse, search & install for free.",
126
+ ad: "DSH-Plugin Hub: {total} plugins indexed, {verified} human-verified, updated daily",
116
127
  search: "Search plugins by name, description, tags…",
117
128
  all: "All",
118
129
  sortStars: "Most stars",
130
+ sortForks: "Most forks",
119
131
  sortUpdated: "Recently updated",
120
132
  sortNewest: "Newest added",
121
- open: "Open in browser",
122
133
  openHint: "Open dsh-plugin.org",
123
- openBtn: "dsh-plugin.org",
124
- toZh: "Switch to Chinese",
134
+ toggleLangHint: "Switch language",
135
+ githubHint: "View source on GitHub",
125
136
  fork: "Fork",
126
137
  loading: "Loading plugin data…",
127
138
  failed: "Failed to load plugin data",
128
139
  failedDesc: "Please retry, or open the hub in your browser with the button above.",
129
140
  retry: "Retry",
130
- copy: "Copy install command",
141
+ install: "Install",
131
142
  toastCopied: "Install command copied — paste it in your dsh terminal",
143
+ installed: "Installed",
144
+ notInstalled: "Not installed",
145
+ filterAllHint: "Show all plugins",
146
+ filterInstalledHint: "Filter to installed plugins in this category",
147
+ filterNotInstalledHint: "Filter to uninstalled plugins in this category",
148
+ filterInstalledNone: "No installed plugins in this category",
149
+ filterNotInstalledNone: "No uninstalled plugins in this category",
150
+ uninstall: "Uninstall",
151
+ uninstalling: "Removing…",
152
+ uninstallTitle: "Confirm uninstall",
153
+ uninstallDesc: "The plugin will be removed from this environment.",
154
+ uninstallDone: "Uninstalled",
155
+ uninstallFail: "Removal failed — try again",
156
+ uninstallResultTitle: "Removed",
157
+ uninstallResultDesc: "The plugin has been removed from this environment.",
158
+ confirmTitle: "Confirm install",
159
+ confirmDesc: "Review the plugin repository source code carefully before installing.",
160
+ confirmPlugin: "Plugin",
161
+ confirmSource: "Source",
162
+ confirmCommand: "Command",
163
+ copyInstallCommand: "Copy install command",
164
+ copyCmdLabel: "Copy",
165
+ copyUninstallCommand: "Copy uninstall command",
166
+ installNow: "Install",
167
+ installing: "Installing…",
168
+ installDone: "Installed",
169
+ installResultTitle: "Plugin installed successfully",
170
+ installResultDesc: "",
171
+ confirmUpdateTitle: "Confirm update",
172
+ updateNow: "Update",
173
+ updating: "Updating…",
174
+ queuedUpdateTitle: "Queued for update…",
175
+ updateResultTitle: "Updated",
176
+ updateResultDesc: "The plugin has been updated to the latest version.",
177
+ installFail: "Install failed — copy the command instead",
178
+ confirmCancel: "Cancel",
179
+ doneBtn: "Done",
180
+ restartNow: "Restart now",
181
+ restartLater: "Later",
182
+ restarting: "Restarting…",
183
+ restartHint: "Some plugins (such as plugin markets / top-level bundles) only take effect after a restart.",
184
+ restartPendingHint: "Installed — takes effect after restart",
185
+ restartPendingHintUninstall: "Removed — cleaned up after restart",
186
+ sectionPendingRestart: "Pending restart",
187
+ sectionInProgress: "In progress",
188
+ queuedTitle: "Queued…",
189
+ queuedUninstallTitle: "Queued for removal…",
190
+ queuedHint: "Queued — starts automatically after earlier tasks finish. You may close this window.",
191
+ cancelTask: "Cancel task",
192
+ cancelling: "Cancelling…",
193
+ errorTitle: "Operation failed",
194
+ errorTitleInstall: "Install failed",
195
+ errorTitleUninstall: "Removal failed",
196
+ errorPlugin: "Plugin",
197
+ errorClose: "Got it",
198
+ errorCopy: "Copy error",
199
+ errCopied: "Error copied",
200
+ notifications: "Notifications",
201
+ notificationsBtn: "Notice",
202
+ notificationsHint: "View install/remove notifications (including failures)",
203
+ notificationsDesc: "Every install and remove result is logged here — even if you missed the prompt, you can always check back.",
204
+ notificationsEmpty: "No notifications yet",
205
+ notificationsClear: "Clear notifications",
206
+ removeNotification: "Remove this notification",
207
+ failCopy: "Copy full log",
208
+ failIssueHint: "Open a pre-filled issue on the author repo with this error log (includes catalog links)",
209
+ failIssueBig: "Report this bug to GitHub — contribute to open source",
210
+ failPrepareHint: "This plugin failed while running its build scripts during install (git tarballs often miss submodules or build output). This is a packaging issue of the plugin itself — please report it to the author repository.",
211
+ failPackagingHint: "This plugin does not support the official default install method: its git distribution lacks the build output (the entry file declared in package.json is not in the repository), so it has not been adapted to the official install flow. Please file an Issue on the author's repo asking for committed build output or an npm release.",
212
+ failIgnoredBuild: "This plugin depends on a native module (e.g. node-pty) whose build script pnpm blocks by default, so the install fails — other plugins are unaffected. This is a dependency/packaging issue of the plugin itself. Please report it to the author repository and ask the author to switch to a prebuilt variant (e.g. node-pty-prebuilt-multiarch).",
213
+ reportIssue: "Report an Issue to the plugin repo",
132
214
  detail: "Open details",
133
215
  noResult: "No plugins match your search",
134
216
  noResultDesc: "Try another keyword, or browse all plugins on the website.",
@@ -136,27 +218,293 @@ window.__ModuleLoader__.load({ id: "dsh-plugin", factory: (require) => {
136
218
  daysAgo: "updated {days}d ago",
137
219
  monthsAgo: "updated {months}mo ago",
138
220
  yearsAgo: "updated {years}y ago",
139
- pluginsTotal: "{n} plugins",
140
- filteredCount: "{n} matched",
221
+ pluginsTotal: "Results: {n} plugins",
222
+ filterResults: "Results: {n} plugins",
141
223
  more: "{n} more on the website",
142
224
  browseAll: "Browse all {n} plugins",
143
225
  verified: "Verified",
226
+ version: "Version",
227
+ update: "Update",
228
+ updateAvailable: "Update available",
229
+ updateAvailableHint: "An update was detected: repos with a release are compared by tag, repos without one by last push time",
144
230
  empty: "No plugins in this category yet",
145
231
  dataFrom: "Data from dsh-plugin.org · curated daily"
146
232
  };
147
233
  //#endregion
148
- //#region src/client/index.tsx
234
+ //#region \0dsh-css:src/client/styles/Header.module.css.mjs
235
+ const css$4 = ".qikqja_root{min-width:0;height:100%;color:var(--hub-text-primary);flex-direction:column;gap:8px;display:flex}.qikqja_header{flex-direction:column;gap:2px;padding:2px 2px 0;display:flex}.qikqja_headerRight{flex-shrink:0;align-items:center;gap:2px;display:inline-flex}.qikqja_langBtn{height:24px;color:var(--hub-text-tertiary);cursor:pointer;-webkit-user-select:none;user-select:none;background:0 0;border:none;border-radius:6px;flex-shrink:0;justify-content:center;align-items:center;padding:0 8px;font-size:12px;font-weight:600;transition:color .15s,background-color .15s;display:inline-flex}.qikqja_langBtn:hover{color:var(--hub-purple-1);background-color:var(--hub-purple-tint)}.qikqja_githubLink{color:var(--hub-text-tertiary);cursor:pointer;border-radius:6px;flex-shrink:0;align-items:center;padding:3px;text-decoration:none;transition:color .15s,background-color .15s;display:inline-flex}.qikqja_githubLink:hover{color:var(--hub-purple-1);background-color:var(--hub-purple-tint)}.qikqja_githubIcon{flex-shrink:0;display:block}.qikqja_headerTitleRow{justify-content:space-between;align-items:center;gap:8px;min-width:0;display:flex}.qikqja_brandTitle{min-width:0;color:inherit;cursor:pointer;align-items:center;gap:10px;text-decoration:none;display:flex}.qikqja_taglineLink{min-width:0;max-width:100%;color:inherit;cursor:pointer;align-self:flex-start;text-decoration:none}.qikqja_title{margin:0;font-size:14px;font-weight:600;line-height:20px}.qikqja_version{color:var(--hub-text-secondary)}.qikqja_logoIcon{flex-shrink:0;display:block}.qikqja_copyIcon{flex-shrink:0}.qikqja_tagline{color:var(--hub-text-tertiary);white-space:nowrap;text-overflow:ellipsis;margin:0;font-size:12px;line-height:18px;overflow:hidden}.qikqja_controls{flex-shrink:0;align-items:center;gap:4px;display:flex}.qikqja_filterResults{color:var(--hub-text-tertiary);white-space:nowrap;flex-shrink:0;align-items:baseline;font-size:12px;line-height:18px;display:inline-flex}.qikqja_resultCount{font-variant-numeric:tabular-nums;text-align:center;min-width:5ch}.qikqja_adBanner{border:1px solid var(--hub-purple-border);background:linear-gradient(90deg, var(--hub-purple-1) 0%, var(--hub-purple-2) 100%);color:#fff;cursor:pointer;-webkit-user-select:none;user-select:none;box-shadow:0 2px 10px var(--hub-purple-shadow);border-radius:8px;align-items:center;gap:8px;padding:7px 12px;text-decoration:none;transition:filter .15s,box-shadow .15s;display:flex}.qikqja_adBanner:hover{filter:brightness(1.1);box-shadow:0 4px 16px var(--hub-purple-shadow-strong)}.qikqja_adBadge{letter-spacing:.04em;color:#fff;white-space:nowrap;background:#ffffff29;border:1px solid #ffffff8c;border-radius:4px;flex-shrink:0;padding:3px 6px;font-size:10px;font-weight:700;line-height:1}.qikqja_adText{text-overflow:ellipsis;white-space:nowrap;color:#fff;min-width:0;font-size:12px;line-height:18px;overflow:hidden}.qikqja_adArrow{color:#fff;flex-shrink:0;margin-left:auto;font-size:13px}.qikqja_searchRow{padding:0 2px;display:flex}.qikqja_search{width:50%;color:inherit;border:1px solid var(--hub-border-2);background:0 0;border-radius:6px;outline:none;padding:4px 9px;font-size:12px;line-height:18px;transition:border-color .12s}.qikqja_search::placeholder{color:var(--hub-text-tertiary)}.qikqja_search:focus{border-color:var(--hub-brand)}.qikqja_installedBtn,.qikqja_installedBtnActive,.qikqja_installedBtnDisabled{cursor:pointer;-webkit-user-select:none;user-select:none;border:none;border-radius:6px;flex-shrink:0;align-items:center;gap:5px;height:24px;padding:0 8px;font-size:12px;line-height:22px;transition:color .12s,background .12s;display:inline-flex}.qikqja_installedBtn{color:var(--hub-text-primary);background:0 0}.qikqja_installedBtn:hover{color:var(--hub-text-primary);background:var(--hub-bg-hover)}.qikqja_installedBtnDisabled,.qikqja_installedBtnDisabled:hover{color:var(--hub-text-disabled);cursor:not-allowed;background:0 0}.qikqja_installedBtnDisabled .qikqja_segCount{color:var(--hub-text-disabled);background:#8080800f}.qikqja_installedBtnActive{color:var(--hub-text-on-fill);background:var(--hub-btn-fill)}.qikqja_segCount,.qikqja_segCountActive{text-align:center;border-radius:999px;min-width:14px;padding:0 4px;font-size:10px;line-height:14px}.qikqja_segCount{color:var(--hub-text-tertiary);background:var(--hub-bg-btn)}.qikqja_installedBtnActive .qikqja_segCountActive{color:var(--hub-text-on-fill);background:var(--hub-bg-on-fill)}.qikqja_tabs{border-bottom:1px solid var(--hub-border-1);flex-wrap:wrap;align-items:center;gap:6px;padding:2px 2px 8px;display:flex}.qikqja_tab,.qikqja_tabActive{cursor:pointer;-webkit-user-select:none;user-select:none;border:none;border-radius:999px;flex-shrink:0;align-items:center;gap:6px;padding:3px 10px;font-size:12px;line-height:18px;transition:color .12s,background .12s;display:inline-flex}.qikqja_tab{color:var(--hub-text-secondary);background:0 0}.qikqja_tab:hover{color:var(--hub-text-primary);background:var(--hub-bg-hover)}.qikqja_tabActive{color:var(--hub-text-on-fill);background:var(--hub-btn-fill)}.qikqja_tabCount{text-align:center;min-width:16px;color:var(--hub-text-tertiary);background:var(--hub-bg-btn);border-radius:999px;padding:0 5px;font-size:10px;line-height:14px}.qikqja_tabActive .qikqja_tabCount{color:var(--hub-text-on-fill);background:var(--hub-bg-on-fill)}.qikqja_failBtn{box-sizing:border-box;border:1px solid var(--hub-border-2);height:24px;color:var(--hub-text-primary);background:var(--hub-bg-hover);cursor:pointer;-webkit-user-select:none;user-select:none;border-radius:6px;flex-shrink:0;align-items:center;margin-left:auto;padding:0 10px;font-size:12px;line-height:22px;transition:color .12s,background .12s;display:inline-flex;position:relative}.qikqja_failBtn:hover{color:var(--hub-text-primary);background:#80808047}.qikqja_failBadge{z-index:1;text-align:center;color:#fff;background:var(--hub-danger);border-radius:999px;min-width:16px;height:16px;padding:0 5px;font-size:10px;font-weight:600;line-height:16px;position:absolute;top:-16px;right:-7px;box-shadow:0 1px 4px #d1242f66}";
236
+ const tagId$4 = "dsh-plugin/Header.module.css";
237
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
238
+ const tag = document.createElement("style");
239
+ tag.dataset.plugin = "dsh-plugin";
240
+ tag.dataset.pluginCss = tagId$4;
241
+ tag.textContent = css$4;
242
+ document.head.appendChild(tag);
243
+ }
244
+ var Header_module_css_default = {
245
+ "adText": "qikqja_adText",
246
+ "tab": "qikqja_tab",
247
+ "copyIcon": "qikqja_copyIcon",
248
+ "filterResults": "qikqja_filterResults",
249
+ "failBadge": "qikqja_failBadge",
250
+ "headerRight": "qikqja_headerRight",
251
+ "brandTitle": "qikqja_brandTitle",
252
+ "taglineLink": "qikqja_taglineLink",
253
+ "search": "qikqja_search",
254
+ "installedBtn": "qikqja_installedBtn",
255
+ "tabCount": "qikqja_tabCount",
256
+ "resultCount": "qikqja_resultCount",
257
+ "segCountActive": "qikqja_segCountActive",
258
+ "adArrow": "qikqja_adArrow",
259
+ "controls": "qikqja_controls",
260
+ "installedBtnDisabled": "qikqja_installedBtnDisabled",
261
+ "root": "qikqja_root",
262
+ "githubLink": "qikqja_githubLink",
263
+ "header": "qikqja_header",
264
+ "logoIcon": "qikqja_logoIcon",
265
+ "tabs": "qikqja_tabs",
266
+ "adBanner": "qikqja_adBanner",
267
+ "failBtn": "qikqja_failBtn",
268
+ "githubIcon": "qikqja_githubIcon",
269
+ "adBadge": "qikqja_adBadge",
270
+ "headerTitleRow": "qikqja_headerTitleRow",
271
+ "tagline": "qikqja_tagline",
272
+ "title": "qikqja_title",
273
+ "installedBtnActive": "qikqja_installedBtnActive",
274
+ "segCount": "qikqja_segCount",
275
+ "langBtn": "qikqja_langBtn",
276
+ "version": "qikqja_version",
277
+ "tabActive": "qikqja_tabActive",
278
+ "searchRow": "qikqja_searchRow"
279
+ };
280
+ //#endregion
281
+ //#region src/client/lib/failures.ts
282
+ const KEY = "gro.ngilp-hsd.failure-records";
283
+ const MAX = 50;
149
284
  /**
150
- * dsh-plugin client: a native "Plugin Hub" settings section that renders
151
- * the community marketplace (dsh-plugin.org) as a first-class catalog —
152
- * category tabs (11 categories + All), search, sorting and one-click
153
- * install-command copy. Plugin data is fetched live from dsh-plugin.org,
154
- * so the browser bundle stays small and always in sync with the site.
155
- *
156
- * Built by tsdown into the __ModuleLoader__ factory bundle at client/client.js.
285
+ * 失败归类,三态。无论底层机制如何(pnpm 白名单拦截 / 构建脚本被忽略 / prepare 失败),
286
+ * 对用户而言结果都一样 —— 当前安装通道(npm 或 git)装不上,就是插件分发/依赖的问题,
287
+ * 一律引导提 Issue:
288
+ * - pnpmIgnoredBuild:插件依赖里的原生模块构建脚本被 pnpm 默认拦截(如 node-pty,
289
+ * `ERR_PNPM_IGNORED_BUILDS`)。只影响带原生模块的插件,其他插件不受影响 —— 差异在
290
+ * 插件的依赖选择,属插件依赖/打包问题 → 引导去仓库提 Issue(建议改用预编译版本)
291
+ * - pluginPrepare:插件的 prepare/构建脚本实际执行失败(git tarball 常因缺失子模块或
292
+ * 构建产物导致)—— 属插件打包/分发问题,应引导去仓库提 Issue
293
+ * - repo:其余失败(含 git prepare 被 pnpm 白名单拦截等),默认按插件仓库问题引导提 Issue
157
294
  */
158
- const NS = "dsh-plugin";
295
+ function classifyFailure(message) {
296
+ if (/\[packaging\]|entry file missing/i.test(message)) return "pluginPrepare";
297
+ if (/ERR_PNPM_IGNORED_BUILDS|Ignored build scripts:/i.test(message)) return "pnpmIgnoredBuild";
298
+ if (/ERR_PNPM_PREPARE_PACKAGE|ELIFECYCLE|Command failed|prepare-guard/i.test(message)) return "pluginPrepare";
299
+ return "repo";
300
+ }
301
+ /** 核心行特征:错误代码 / 生命周期脚本失败 / prepare 失败 / 描述性报错(子模块缺失、找不到等)/ 退出与宿主提示信息。 */
302
+ const CORE_LINE_RE = /ERR_[A-Z_]+|ELIFECYCLE|Command failed|prepare-guard|Failed to prepare|exit code|\bprepare\b|pnpm failed in profile|git-hosted plugins build|submodule|not found|cannot find|no such|unable to|fatal|missing|error/i;
303
+ /** 提交 issue 时正文里错误摘要的上限字符数。GitHub 请求行上限 8192 字节,
304
+ * 固定模板与 URL 编码开销约 1~2K,核心错误(以 ASCII 日志为主)可安全带到 ~5K;
305
+ * 仍超长时 pluginIssueUrl 会逐档缩小核心预算,最终 URL 不会超限。 */
306
+ const MAX_CORE_CHARS = 5e3;
307
+ /** 摘要里单行上限:允许构建 key 等长行也被截短。 */
308
+ const MAX_LINE_CHARS = 400;
309
+ /**
310
+ * 核心错误收集器:从完整安装输出里挑出真正说明问题的行(错误代码、构建脚本失败、
311
+ * 退出与宿主提示),去重后拼接,单行与总量都截断 —— 只把「重点 + 原因」带进 issue 正文,
312
+ * 避免完整日志塞进 URL 导致请求过长。无关键行时退化为「头部 + 尾部」快照。
313
+ * maxChars 可由调用方按最终 URL 长度收紧(pluginIssueUrl 超限时逐档缩小)。
314
+ */
315
+ function summarizeError(message, maxChars = MAX_CORE_CHARS) {
316
+ const seen = /* @__PURE__ */ new Set();
317
+ const core = [];
318
+ for (const raw of message.split(/\r?\n/)) {
319
+ const line = raw.trim();
320
+ if (!line || !CORE_LINE_RE.test(line)) continue;
321
+ const short = line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}…` : line;
322
+ if (!seen.has(short)) {
323
+ seen.add(short);
324
+ core.push(short);
325
+ }
326
+ }
327
+ let out;
328
+ if (core.length === 0) {
329
+ const tail = message.trimEnd().slice(-1e3);
330
+ const sep = "\n…\n";
331
+ const headBudget = Math.max(maxChars - tail.length - 3, 0);
332
+ const head = message.slice(0, 500).trim();
333
+ out = `${head.length > headBudget ? head.slice(0, headBudget) : head}${sep}${tail}`;
334
+ } else out = core.join("\n");
335
+ return out.length > maxChars ? `${out.slice(0, maxChars)}\n… (truncated)` : out;
336
+ }
337
+ /** 提取首个错误代码(如 ERR_PNPM_PREPARE_PACKAGE),无则 null。 */
338
+ function coreErrorCode(message) {
339
+ const m = message.match(/\[?ERR_[A-Z_]+\]?/);
340
+ return m ? m[0].replace(/^\[|\]$/g, "") : null;
341
+ }
342
+ /** 运行时 localStorage 访问:类型上不依赖 DOM lib(Node 测试环境也能编译),浏览器里取 window。 */
343
+ const storage = () => globalThis.localStorage;
344
+ /** 读取本地通知记录(损坏/不可用时返回空列表,不抛错)。列表最新在前,全部保留,只由「清空」按钮手动移除。 */
345
+ function loadNotifications() {
346
+ try {
347
+ const raw = storage()?.getItem(KEY);
348
+ if (!raw) return [];
349
+ const list = JSON.parse(raw);
350
+ if (!Array.isArray(list)) return [];
351
+ return list.filter((r) => !!r && typeof r === "object" && typeof r.message === "string").map((r) => ({
352
+ ...r,
353
+ ok: r.ok === true
354
+ }));
355
+ } catch {
356
+ return [];
357
+ }
358
+ }
359
+ function save(list) {
360
+ try {
361
+ storage()?.setItem(KEY, JSON.stringify(list));
362
+ } catch {}
363
+ }
364
+ /** 追加一条通知记录并持久化,返回更新后的完整列表(最新在前,超上限裁剪)。
365
+ * 每次成功/失败都各自留痕:同一插件的安装/卸载记录都完整保留、只由「清空」按钮手动移除,
366
+ * 不会因后续操作被自动覆盖清除。 */
367
+ function addNotification(record) {
368
+ const prev = loadNotifications();
369
+ let id = Date.now();
370
+ while (prev.some((r) => r.id === id)) id += 1;
371
+ const next = [{
372
+ ...record,
373
+ id,
374
+ at: id
375
+ }, ...prev].slice(0, MAX);
376
+ save(next);
377
+ return next;
378
+ }
379
+ /** 记录一次失败:携带完整错误日志,供通知中心查看/复制/提 Issue。 */
380
+ function addFailure(record) {
381
+ return addNotification({
382
+ ...record,
383
+ ok: false
384
+ });
385
+ }
386
+ /** 记录一次成功:轻量记录,不带日志。 */
387
+ function addSuccess(record) {
388
+ return addNotification({
389
+ ...record,
390
+ ok: true,
391
+ message: ""
392
+ });
393
+ }
394
+ /** 清空全部通知记录,返回空列表。 */
395
+ function clearNotifications() {
396
+ save([]);
397
+ return [];
398
+ }
399
+ /** 按 id 删除单条通知记录,返回更新后的列表(仅移除该条,不影响其余记录)。 */
400
+ function removeNotification(id) {
401
+ const next = loadNotifications().filter((r) => r.id !== id);
402
+ save(next);
403
+ return next;
404
+ }
405
+ //#endregion
406
+ //#region src/client/lib/catalog.ts
407
+ /** 插件中心官网地址。 */
159
408
  const SITE_URL = "https://dsh-plugin.org/";
409
+ /** 插件中心源码仓库:头部右上角 GitHub 图标的跳转地址 */
410
+ const GITHUB_URL = "https://github.com/dshplugin/dsh-plugin-hub";
411
+ /**
412
+ * 插件当前版本号,供头部标题展示「DSH-Plugin Hub v0.1.1」。
413
+ * tsdown 构建时用 define 把 __PLUGIN_VERSION__ 替换成 package.json 的版本号;
414
+ * node --test 直接 import 本模块时该标识符不存在,typeof 守卫兜底为空串。
415
+ */
416
+ const PLUGIN_VERSION = "1.1.0";
417
+ /** dsh-plugin.org 中文页挂在 /zh/ 前缀下,英文在根路径。 */
418
+ function langPathOf(lang) {
419
+ return lang === "zh" ? "zh/" : "";
420
+ }
421
+ /**
422
+ * 官网详情页两级路径:/plugins/{ownerSlug}/{slug};
423
+ * 旧数据缺 ownerSlug 时从 repo 推导(卡片详情按钮与弹窗来源行共用)。
424
+ */
425
+ function pluginDetailUrl(plugin, langPath) {
426
+ const repo = plugin.source?.repo ?? "";
427
+ return `${SITE_URL}${langPath}plugins/${plugin.ownerSlug ?? repo.split("/")[0]?.toLowerCase() ?? ""}/${plugin.slug}`;
428
+ }
429
+ /**
430
+ * 失败弹窗里仓库地址的跳转目标:官网详情页(含插件收录信息),
431
+ * 与 issue 正文的 Catalog 链接同一形式(单级 /plugins/{repo})。
432
+ */
433
+ function pluginSiteUrl(repo) {
434
+ return `${SITE_URL}plugins/${repo}`;
435
+ }
436
+ /**
437
+ * 一键反馈 GitHub Issue 的预填链接:标题带「来自 dsh-plugin.org」标识,正文只带
438
+ * 核心信息 —— 原因判定 + 关键错误代码 + 尝试过的安装方式 + 宿主机器环境快照 +
439
+ * 错误核心摘要(完整日志太长,塞进 URL 会被 GitHub 以「request URL too long」拒绝,
440
+ * 故只收集重点)。错误弹窗、失败记录共用此逻辑。
441
+ * env 取不到时为 null,链接照常生成、只是少环境段。
442
+ * attempts(尝试过的安装方式,npm 反查 + 实际执行命令):作者看到我们查过/试过的命令,
443
+ * 组织 scope 与 GitHub 用户名不一致时也能直接指认正确的 npm 包名。
444
+ */
445
+ function pluginIssueUrl(repo, message, env, command, attempts) {
446
+ const title = `[dsh-plugin.org | dsh-plugin-hub] Install/Remove failed: ${repo}`;
447
+ const kind = classifyFailure(message);
448
+ const reason = /\[packaging\]/i.test(message) ? "plugin distribution is incomplete — the entry file declared in package.json is missing from the published package (github tarball or npm package); please commit build output or publish a complete package" : kind === "pluginPrepare" ? "plugin prepare/build script failed during install (packaging/distribution issue)" : kind === "pnpmIgnoredBuild" ? "plugin depends on a native module whose build script pnpm blocks by default (use a prebuilt variant)" : "plugin-side install failure";
449
+ const code = coreErrorCode(message);
450
+ const build = (coreChars) => {
451
+ const body = [
452
+ "## Summary",
453
+ `- Cause: ${reason}`,
454
+ ...code ? [`- Key error: \`${code}\``] : [],
455
+ "",
456
+ `## [DSH-Plugin 插件中心](${SITE_URL}) · 安装 Plugin 失败错误信息`,
457
+ `本错误信息由 [dsh-plugin-hub](${GITHUB_URL}) 插件中心的安装程序自动生成,随本次安装失败一并提交。`,
458
+ `- 实际执行的安装命令:\`${command ?? `dsh plugin${env?.profile ? ` --profile ${env.profile}` : ""} add github:${repo}`}\``,
459
+ `- 执行结果:安装失败,未能安装该插件。`,
460
+ ...attempts && attempts.length > 0 ? [
461
+ "",
462
+ "## Attempted install channels(已尝试的安装方式)",
463
+ ...attempts.map((a) => `- ${a}`)
464
+ ] : [],
465
+ .../\[packaging\]/i.test(message) ? [
466
+ "",
467
+ "## 安装方式说明",
468
+ "DSH 插件支持两种官方安装通道:`dsh plugin add <npm-package>`(npm 分发,需发布完整构建产物)与 `dsh plugin add github:owner/repo`(git 直装,仓库需提交构建产物或在 package.json 提供 `prepare` 脚本)。当前插件的分发物缺少 package.json 声明的入口文件,请按所用通道补齐后重新发布。"
469
+ ] : [],
470
+ "",
471
+ "## Environment",
472
+ `- Plugin: \`${repo}\``,
473
+ ...env ? [
474
+ `- DSH: ${env.dshVersion ? `v${env.dshVersion}` : "unknown"}`,
475
+ `- Plugin Hub: v${PLUGIN_VERSION}`,
476
+ `- Node: ${env.nodeVersion}`,
477
+ `- OS: ${env.platform} ${env.arch} (${env.release})`,
478
+ `- Profile: \`${env.profile}\``,
479
+ `- DSH Home: \`${env.dshHome}\``
480
+ ] : [],
481
+ `- Catalog: [${pluginSiteUrl(repo)}](${pluginSiteUrl(repo)})`,
482
+ "",
483
+ "## Error (core)",
484
+ "",
485
+ "```",
486
+ summarizeError(message, coreChars),
487
+ "```",
488
+ "",
489
+ "---",
490
+ `This error was produced by the [dsh-plugin-hub](${GITHUB_URL}) installer bundled with DeepSeek Harness.`
491
+ ].join("\n");
492
+ return `https://github.com/${repo}/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
493
+ };
494
+ const MAX_URL_CHARS = 7600;
495
+ let url = build(MAX_CORE_CHARS);
496
+ if (url.length > MAX_URL_CHARS) for (const budget of [
497
+ 4e3,
498
+ 2500,
499
+ 1400,
500
+ 800,
501
+ 400
502
+ ]) {
503
+ url = build(budget);
504
+ if (url.length <= MAX_URL_CHARS) break;
505
+ }
506
+ return url;
507
+ }
160
508
  const CATEGORY_ORDER = [
161
509
  "interface",
162
510
  "session",
@@ -170,20 +518,28 @@ window.__ModuleLoader__.load({ id: "dsh-plugin", factory: (require) => {
170
518
  "knowledge",
171
519
  "fun"
172
520
  ];
521
+ /** 分类标签按界面语言取词;未知 key 原样返回。 */
522
+ function categoryLabel(map, key, lang) {
523
+ return map[key]?.[lang] ?? key;
524
+ }
173
525
  /**
174
526
  * 归一化在线 API(dsh-plugin.org/api/plugins.{lang}.json)返回的短 key 结构
175
- * (s/o/n/c/t/f/d/r/v/u/a/sg/fk),统一为 HubPlugin,保证渲染逻辑只认一种结构。
527
+ * (s/o/n/vr/c/t/f/d/r/v/u/a/sg/fk),统一为 HubPlugin,保证渲染逻辑只认一种结构。
176
528
  */
177
529
  function normalize(raw) {
178
530
  if (typeof raw.s === "string") return {
179
531
  slug: raw.s,
180
532
  ownerSlug: typeof raw.o === "string" ? raw.o : void 0,
181
533
  displayName: typeof raw.n === "string" ? raw.n : void 0,
534
+ version: typeof raw.vr === "string" ? raw.vr : void 0,
182
535
  category: typeof raw.c === "string" ? raw.c : void 0,
183
536
  topics: Array.isArray(raw.t) ? raw.t : void 0,
184
537
  features: Array.isArray(raw.f) ? raw.f : void 0,
185
538
  description: typeof raw.d === "string" ? raw.d : void 0,
186
- source: typeof raw.r === "string" ? { repo: raw.r } : void 0,
539
+ source: typeof raw.r === "string" ? { repo: raw.r } : raw.r !== null && typeof raw.r === "object" ? {
540
+ repo: typeof raw.r.repo === "string" ? raw.r.repo : void 0,
541
+ npmPackage: typeof raw.r.npmPackage === "string" ? raw.r.npmPackage : void 0
542
+ } : void 0,
187
543
  compatibility: typeof raw.v === "string" ? { status: raw.v } : void 0,
188
544
  dates: {
189
545
  repoUpdatedAt: typeof raw.u === "string" ? raw.u : void 0,
@@ -196,6 +552,28 @@ window.__ModuleLoader__.load({ id: "dsh-plugin", factory: (require) => {
196
552
  };
197
553
  return raw;
198
554
  }
555
+ /**
556
+ * 安装通道决策(用户无感知):目录探测到 npm 包名 → 用 npm 包名安装
557
+ * (走 npm registry tarball,更快、与 GitHub 网络无关);无 npm 包名 → git 直装。
558
+ * 返回值 target 即传给后端 /install 的安装目标(npm 包名 或 owner/repo)。
559
+ */
560
+ function installTargetOf(p) {
561
+ const pkg = (p.source?.npmPackage ?? "").trim();
562
+ const repo = (p.source?.repo ?? "").trim();
563
+ if (pkg && repo) return {
564
+ target: pkg,
565
+ via: "npm"
566
+ };
567
+ return {
568
+ target: repo,
569
+ via: "github"
570
+ };
571
+ }
572
+ /** 展示用安装命令(复制/弹窗):npm 通道显示包名,git 通道显示 github: 源。 */
573
+ function installCommandOf(p, withProfile = false) {
574
+ const { target, via } = installTargetOf(p);
575
+ return via === "npm" ? `dsh plugin${withProfile ? " --profile web" : ""} add ${target}` : `dsh plugin${withProfile ? " --profile web" : ""} add github:${target}`;
576
+ }
199
577
  const CATEGORY_LABELS = {
200
578
  interface: {
201
579
  zh: "界面与体验",
@@ -242,6 +620,7 @@ window.__ModuleLoader__.load({ id: "dsh-plugin", factory: (require) => {
242
620
  en: "Entertainment"
243
621
  }
244
622
  };
623
+ /** 分类 Tab 的紧凑标签(英文模式空间受限时使用)。 */
245
624
  const CATEGORY_SHORT_LABELS = {
246
625
  interface: {
247
626
  zh: "界面体验",
@@ -290,25 +669,30 @@ window.__ModuleLoader__.load({ id: "dsh-plugin", factory: (require) => {
290
669
  };
291
670
  const SORTS = [
292
671
  "sortStars",
672
+ "sortForks",
293
673
  "sortUpdated",
294
674
  "sortNewest"
295
675
  ];
296
- function fmtStars(count) {
297
- if (!count || count <= 0) return "0";
298
- if (count < 1e3) return String(count);
299
- const k = count / 1e3;
300
- return `${k >= 100 ? Math.round(k) : k.toFixed(1).replace(/\.0$/, "")}k`;
301
- }
302
- function relTime(iso, t) {
303
- if (!iso) return "";
304
- const days = Math.floor((Date.now() - new Date(iso).getTime()) / 864e5);
305
- if (days <= 0) return t("today");
306
- if (days < 30) return t("daysAgo", { days });
307
- if (days < 365) return t("monthsAgo", { months: Math.floor(days / 30) });
308
- return t("yearsAgo", { years: Math.floor(days / 365) });
676
+ //#endregion
677
+ //#region src/client/lib/env.ts
678
+ let envPromise = null;
679
+ function getEnv() {
680
+ envPromise ??= fetch("/dsh-plugin-hub/env", { cache: "no-store" }).then((res) => res.ok ? res.json() : null).catch(() => null);
681
+ return envPromise;
309
682
  }
310
- function PluginHubSection({ t: _hostT, locale }) {
311
- const [lang, setLang] = (0, react.useState)(locale.getSnapshot().active);
683
+ //#endregion
684
+ //#region src/client/hooks/useCatalog.ts
685
+ /**
686
+ * Catalog data + view state for the Plugin Hub section.
687
+ *
688
+ * Owns the online-data pipeline (live fetch from dsh-plugin.org), the local
689
+ * installed-plugin table, and the filter/search/sort/install-status view
690
+ * state, exposing the derived visible list and per-category counts.
691
+ */
692
+ const PLUGINS_URL = (lang) => `https://dsh-plugin.org/api/plugins.${lang}.json`;
693
+ const STATS_URL = "https://dsh-plugin.org/api/stats.json";
694
+ function useCatalog(lang) {
695
+ /** 目录插件(仅保留人工验证通过的条目) */
312
696
  const [plugins, setPlugins] = (0, react.useState)(null);
313
697
  /** 收录/精选统计(官网 /api/stats.json 实时拉取) */
314
698
  const [stats, setStats] = (0, react.useState)(null);
@@ -317,50 +701,118 @@ window.__ModuleLoader__.load({ id: "dsh-plugin", factory: (require) => {
317
701
  const [category, setCategory] = (0, react.useState)("all");
318
702
  const [query, setQuery] = (0, react.useState)("");
319
703
  const [sort, setSort] = (0, react.useState)("sortStars");
320
- const [copied, setCopied] = (0, react.useState)(null);
321
- /** 全局复制成功 Toast:{id} 用于重复点击时重新触发入场动画 */
322
- const [toast, setToast] = (0, react.useState)(null);
323
- /** 列表滚动容器:分类/搜索切换后列表内容替换但 scrollTop 保留,会让用户误以为列表没更新,需重置回顶部 */
324
- const listRef = (0, react.useRef)(null);
325
- (0, react.useEffect)(() => {
326
- listRef.current?.scrollTo({ top: 0 });
327
- }, [category]);
328
- (0, react.useEffect)(() => locale.subscribe(() => setLang(locale.getSnapshot().active)), [locale]);
704
+ /** 列表安装状态筛选:全部 / 已安装 / 未安装(分段按钮,单列表内切换,不引入第二个列表) */
705
+ const [installedFilter, setInstalledFilter] = (0, react.useState)("all");
706
+ /** 当前 profile 已安装插件:npm 包名 -> manifest spec(来自宿主本地路由) */
707
+ const [installed, setInstalled] = (0, react.useState)({});
708
+ /** 安装时记录的目录信号:repo(小写) -> { version, updatedAt }(来自宿主本地路由);
709
+ * npmPackage 为 npm 优先通道反查命中的包名映射(目录数据未下发时客户端靠它把依赖 key 匹配回仓库) */
710
+ const [versions, setVersions] = (0, react.useState)({});
329
711
  (0, react.useEffect)(() => {
330
712
  let cancelled = false;
331
713
  setPlugins(null);
332
714
  setStats(null);
333
715
  setFailed(false);
334
- const apply = (data) => {
716
+ const fetchData = (url) => fetch(url, { cache: "no-store" }).then((res) => {
717
+ if (!res.ok) throw new Error(String(res.status));
718
+ return res.json();
719
+ });
720
+ fetchData(PLUGINS_URL(lang)).then((data) => {
335
721
  if (cancelled) return;
336
722
  const list = (Array.isArray(data) ? data : []).map((item) => normalize(item));
337
723
  setPlugins(list.filter((p) => p.compatibility?.status === "verified"));
338
- };
339
- const fail = () => {
724
+ }).catch(() => {
340
725
  if (!cancelled) setFailed(true);
341
- };
342
- const fetchData = (url) => fetch(url).then((res) => {
343
- if (!res.ok) throw new Error(String(res.status));
344
- return res.json();
345
726
  });
346
- fetchData(`https://dsh-plugin.org/api/plugins.${lang}.json`).then(apply).catch(fail);
347
- const applyStats = (s) => {
348
- if (!cancelled && s && typeof s.total === "number" && typeof s.verified === "number") setStats(s);
349
- };
350
- fetchData("https://dsh-plugin.org/api/stats.json").then((s) => applyStats(s)).catch(() => {});
727
+ fetchData(STATS_URL).then((s) => {
728
+ const stats = s;
729
+ if (!cancelled && stats && typeof stats.total === "number" && typeof stats.verified === "number") setStats({
730
+ total: stats.total,
731
+ verified: stats.verified
732
+ });
733
+ }).catch(() => {});
351
734
  return () => {
352
735
  cancelled = true;
353
736
  };
354
- }, [lang, reloadKey]);
737
+ }, [reloadKey, lang]);
738
+ /** 刷新当前 profile 已安装插件表;宿主未挂本地路由时静默降级为空表。 */
739
+ const refreshInstalled = async () => {
740
+ try {
741
+ const res = await fetch("/dsh-plugin-hub/installed", { cache: "no-store" });
742
+ if (!res.ok) return;
743
+ const data = await res.json();
744
+ setInstalled(data.installed ?? {});
745
+ setVersions(data.versions ?? {});
746
+ } catch {}
747
+ };
748
+ (0, react.useEffect)(() => {
749
+ refreshInstalled();
750
+ }, []);
751
+ /** 插件是否已安装:匹配 installed spec 中的 `github:<owner>/<repo>`,或 npm 通道安装的依赖包名;命中返回 npm 包名。 */
752
+ const installedName = (p) => {
753
+ const repo = p.source?.repo;
754
+ if (!repo) return null;
755
+ const needle = `github:${repo.toLowerCase()}`;
756
+ for (const [name, spec] of Object.entries(installed)) if (spec.toLowerCase().includes(needle)) return name;
757
+ const pkg = ((p.source?.npmPackage || versions[repo.toLowerCase()]?.npmPackage) ?? "").toLowerCase();
758
+ if (pkg) {
759
+ for (const name of Object.keys(installed)) if (name.toLowerCase() === pkg) return name;
760
+ }
761
+ return null;
762
+ };
763
+ /** 该插件安装时记录的目录版本(无记录/未安装 → null)。 */
764
+ const installedVersion = (p) => {
765
+ const repo = p.source?.repo;
766
+ if (!repo) return null;
767
+ return versions[repo.toLowerCase()]?.version ?? null;
768
+ };
769
+ /**
770
+ * 是否有更新:仅对已安装插件有意义。
771
+ * 双信号判定——有 release 版本的比版本;无版本(repo 不打 tag)的比仓库最近更新时间
772
+ * (repoUpdatedAt,ISO 字符串字典序 = 时间序),更新时间变新说明有新提交。
773
+ */
774
+ const hasUpdate = (p) => {
775
+ if (installedName(p) === null) return false;
776
+ const repo = p.source?.repo;
777
+ if (!repo) return false;
778
+ const rec = versions[repo.toLowerCase()];
779
+ if (!rec) return false;
780
+ if (p.version) return p.version !== rec.version;
781
+ const current = p.dates?.repoUpdatedAt;
782
+ return Boolean(current && rec.updatedAt && current > rec.updatedAt);
783
+ };
784
+ /** 当前分类下的插件(「全部」时为整个目录)。 */
785
+ const categoryPlugins = (0, react.useMemo)(() => {
786
+ if (!plugins) return [];
787
+ if (category === "all") return plugins;
788
+ return plugins.filter((p) => p.category === category);
789
+ }, [plugins, category]);
790
+ /** 当前分类下已安装插件数:已安装/未安装按钮上的计数跟随分类,不再用全局口径。 */
791
+ const installedCountInCategory = (0, react.useMemo)(() => {
792
+ return categoryPlugins.reduce((n, p) => n + (installedName(p) !== null ? 1 : 0), 0);
793
+ }, [categoryPlugins, installed]);
794
+ /** 当前分类下未安装插件数。 */
795
+ const notInstalledCountInCategory = Math.max(0, categoryPlugins.length - installedCountInCategory);
796
+ (0, react.useEffect)(() => {
797
+ if (installedFilter === "all") return;
798
+ if ((installedFilter === "installed" ? installedCountInCategory : notInstalledCountInCategory) === 0) setInstalledFilter("all");
799
+ }, [
800
+ installedFilter,
801
+ installedCountInCategory,
802
+ notInstalledCountInCategory
803
+ ]);
355
804
  const visible = (0, react.useMemo)(() => {
356
805
  if (!plugins) return [];
357
806
  const q = query.trim().toLowerCase();
358
807
  return [...plugins.filter((p) => {
359
808
  if (category !== "all" && p.category !== category) return false;
809
+ if (installedFilter === "installed" && installedName(p) === null) return false;
810
+ if (installedFilter === "notInstalled" && installedName(p) !== null) return false;
360
811
  if (!q) return true;
361
812
  return (p.displayName ?? "").toLowerCase().includes(q) || (p.description ?? "").toLowerCase().includes(q) || (p.topics ?? []).some((topic) => topic.toLowerCase().includes(q));
362
813
  })].sort((a, b) => {
363
814
  if (sort === "sortStars") return (b.stats?.stargazers_count ?? 0) - (a.stats?.stargazers_count ?? 0);
815
+ if (sort === "sortForks") return (b.stats?.forks_count ?? 0) - (a.stats?.forks_count ?? 0);
364
816
  if (sort === "sortNewest") return (b.dates?.addedAt ?? "").localeCompare(a.dates?.addedAt ?? "");
365
817
  return (b.dates?.repoUpdatedAt ?? "").localeCompare(a.dates?.repoUpdatedAt ?? "");
366
818
  });
@@ -368,22 +820,1515 @@ window.__ModuleLoader__.load({ id: "dsh-plugin", factory: (require) => {
368
820
  plugins,
369
821
  category,
370
822
  query,
371
- sort
823
+ sort,
824
+ installed,
825
+ installedFilter
372
826
  ]);
827
+ /** Per-category plugin counts shown on the category chips. */
828
+ const categoryCounts = (0, react.useMemo)(() => {
829
+ const counts = {};
830
+ for (const p of plugins ?? []) if (p.category) counts[p.category] = (counts[p.category] ?? 0) + 1;
831
+ return counts;
832
+ }, [plugins]);
833
+ return {
834
+ plugins,
835
+ stats,
836
+ failed,
837
+ reload: () => setReloadKey((k) => k + 1),
838
+ installed,
839
+ installedName,
840
+ installedVersion,
841
+ hasUpdate,
842
+ refreshInstalled,
843
+ category,
844
+ setCategory,
845
+ query,
846
+ setQuery,
847
+ sort,
848
+ setSort,
849
+ installedFilter,
850
+ setInstalledFilter,
851
+ visible,
852
+ total: plugins?.length ?? 0,
853
+ installedCountInCategory,
854
+ notInstalledCountInCategory,
855
+ categoryCounts
856
+ };
857
+ }
858
+ //#endregion
859
+ //#region src/client/hooks/useTaskQueue.ts
860
+ /**
861
+ * Background install/remove task queue for the Plugin Hub.
862
+ *
863
+ * Mirrors the server-side FIFO active queue by polling /active, settles
864
+ * vanished tasks against /status, and exposes the queue actions (install,
865
+ * uninstall, cancel) plus the modal task lookups used by the dialogs.
866
+ */
867
+ function useTaskQueue(opts) {
868
+ const { t, refreshInstalled, onInstallDone, onUninstallDone, onError, installPlugin, uninstallPlugin, installedName, resolvePending } = opts;
869
+ const [queue, setQueue] = (0, react.useState)([]);
870
+ const queueRef = (0, react.useRef)([]);
871
+ /** 待重启列表(服务端内存态 + 本地展示信息补齐);与 queue 一样以 ref 为唯一事实来源。 */
872
+ const [pendingRestarts, setPendingRestarts] = (0, react.useState)([]);
873
+ const pendingRef = (0, react.useRef)([]);
874
+ /** 本次会话内安装成功插件的展示信息(目标 → 简介/版本):轮询合并待重启列表时补齐,刷新后由 resolvePending 兜底。 */
875
+ const pendingInfoRef = (0, react.useRef)(/* @__PURE__ */ new Map());
876
+ /** 保持最新的 resolvePending:轮询闭包捕获的是调用那一刻的目录状态,目录晚加载时也能补齐。 */
877
+ const resolvePendingRef = (0, react.useRef)(resolvePending);
878
+ resolvePendingRef.current = resolvePending;
879
+ /** 当前打开弹窗所对应的任务 id:该任务完成时弹窗切换为结果视图 */
880
+ const modalTaskRef = (0, react.useRef)(null);
881
+ /** 已开始收尾但尚未结束的任务 id(服务端 /active 已消失 → 查 /status 中):并发轮询时跳过,避免重复收尾 */
882
+ const settlingRef = (0, react.useRef)(/* @__PURE__ */ new Set());
883
+ /** 轮询定时器句柄(队列清空/组件卸载时清理) */
884
+ const pollRef = (0, react.useRef)(null);
885
+ /** 请求在途的目标集合(同步防重):双击安装/卸载时第二击直接忽略。
886
+ * 本地队列任务要等 fetch 返回后才入队,仅靠 queueRef 检查拦不住请求窗口内的重复点击。 */
887
+ const submittingRef = (0, react.useRef)(/* @__PURE__ */ new Set());
888
+ /** 乐观入队临时 id 计数器:递减产生唯一负数,与服务端自增正数 id 永不冲突。 */
889
+ const tempIdRef = (0, react.useRef)(0);
890
+ /** 请求在途标记:弹窗据此禁用确认按钮,避免等待响应期间被再次点击。 */
891
+ const [submitting, setSubmitting] = (0, react.useState)(false);
892
+ /** 停止后台任务轮询(任务结束或组件卸载时清理)。 */
893
+ const stopPoll = () => {
894
+ if (pollRef.current !== null) {
895
+ window.clearInterval(pollRef.current);
896
+ pollRef.current = null;
897
+ }
898
+ };
899
+ /** 队列与待重启都清空才停轮询:待重启期间持续轮询,宿主一重启(内存列表清空)立刻感知并消失。 */
900
+ const maybeStopPoll = () => {
901
+ if (queueRef.current.length === 0 && pendingRef.current.length === 0) stopPoll();
902
+ };
903
+ (0, react.useEffect)(() => stopPoll, []);
904
+ /** 更新队列(同步镜像到 ref:queueRef 是唯一事实来源,state 只负责触发渲染,
905
+ * 避免依赖 React updater 的异步调度时序导致 gone/stopPoll/防重复入队读到旧值)。 */
906
+ const applyQueue = (updater) => {
907
+ const next = updater(queueRef.current);
908
+ queueRef.current = next;
909
+ setQueue(next);
910
+ };
911
+ /** 解析服务端待重启列表(`github:` 前缀已去掉,与安装任务的 target 对齐)。 */
912
+ const parsePendingRestarts = (value) => {
913
+ if (!Array.isArray(value)) return [];
914
+ const out = [];
915
+ for (const x of value) {
916
+ if (typeof x !== "object" || x === null) continue;
917
+ const target = x.target;
918
+ if (typeof target !== "string" || target === "") continue;
919
+ const at = x.at;
920
+ out.push({
921
+ target: target.replace(/^github:/, ""),
922
+ kind: x.kind === "uninstall" ? "uninstall" : "install",
923
+ at: typeof at === "number" ? at : 0
924
+ });
925
+ }
926
+ return out;
927
+ };
928
+ /** 合并服务端待重启列表:保留已有展示信息,缺失时用会话内记录 / 目录解析补齐。 */
929
+ const applyPending = (raw) => {
930
+ const prev = pendingRef.current;
931
+ const next = raw.map((p) => {
932
+ const old = prev.find((q) => q.target === p.target);
933
+ if (old?.desc !== void 0) return {
934
+ ...p,
935
+ desc: old.desc,
936
+ version: old.version
937
+ };
938
+ const info = pendingInfoRef.current.get(p.target) ?? resolvePendingRef.current?.(p.target) ?? null;
939
+ return info ? {
940
+ ...p,
941
+ desc: info.desc,
942
+ version: info.version
943
+ } : p;
944
+ });
945
+ pendingRef.current = next;
946
+ setPendingRestarts(next);
947
+ };
948
+ /** 向宿主记录某插件的安装时目录信号(version + updatedAt);version 为空时清除记录(卸载)。 */
949
+ const syncInstalledVersion = async (repo, version, updatedAt) => {
950
+ if (!repo) return;
951
+ try {
952
+ await fetch("/dsh-plugin-hub/installed-version", {
953
+ method: "POST",
954
+ headers: { "content-type": "application/json" },
955
+ body: JSON.stringify({
956
+ repo,
957
+ version: version ?? null,
958
+ updatedAt
959
+ })
960
+ });
961
+ } catch {}
962
+ };
963
+ /** 后台任务结束(active 中消失后查终态确认):完成 → 刷新安装表 + 结果视图/toast;失败 → 完整错误弹窗 */
964
+ const finishQueueTask = (ok, q, lines) => {
965
+ applyQueue((prev) => prev.filter((x) => x.id !== q.id));
966
+ if (ok) {
967
+ refreshInstalled();
968
+ if (q.kind === "uninstall") {
969
+ syncInstalledVersion(q.repo, void 0, void 0);
970
+ onUninstallDone(modalTaskRef.current === q.id, q.repo);
971
+ } else {
972
+ syncInstalledVersion(q.repo, q.version, q.updatedAt);
973
+ pendingInfoRef.current.set(q.target, {
974
+ desc: q.desc,
975
+ version: q.version
976
+ });
977
+ onInstallDone(modalTaskRef.current === q.id, q.repo);
978
+ }
979
+ } else {
980
+ const detail = lines.length > 0 ? [...lines].reverse().join("\n") : q.kind === "uninstall" ? t("uninstallFail") : t("installFail");
981
+ onError(detail, q.repo, q.kind, q.command, q.attempts);
982
+ }
983
+ maybeStopPoll();
984
+ };
985
+ /** 卸载成功收尾前的进度过渡:任务已在服务端结束,但保留本地展示,
986
+ * 让进度条在 ~2.4s 内缓缓跑到 100% 再切结果/Toast,给用户一个交互过程,
987
+ * 而不是命令一执行进度条就一闪消失。 */
988
+ const settleDone = (q, lines) => {
989
+ stopPoll();
990
+ const from = Math.min(q.progress, 90);
991
+ const steps = 20;
992
+ const stepMs = 120;
993
+ let step = 0;
994
+ applyQueue((prev) => prev.map((x) => x.id === q.id ? {
995
+ ...x,
996
+ status: "running"
997
+ } : x));
998
+ const timer = window.setInterval(() => {
999
+ step += 1;
1000
+ const progress = Math.round(from + (100 - from) * (step / steps));
1001
+ applyQueue((prev) => prev.map((x) => x.id === q.id ? {
1002
+ ...x,
1003
+ progress
1004
+ } : x));
1005
+ if (step >= steps) {
1006
+ window.clearInterval(timer);
1007
+ finishQueueTask(true, q, lines);
1008
+ if (queueRef.current.length > 0) pollQueue();
1009
+ }
1010
+ }, stepMs);
1011
+ };
1012
+ /** 任务在 /active 中消失 = 已结束:查 /status 拿终态并收尾(cancelled 静默移除)。 */
1013
+ const settleTask = async (q) => {
1014
+ let status = "failed";
1015
+ let lines = [];
1016
+ let attempts;
1017
+ try {
1018
+ const res = await fetch(`/dsh-plugin-hub/status?task=${q.id}`, { cache: "no-store" });
1019
+ if (res.ok) {
1020
+ const data = await res.json();
1021
+ status = data.task?.status ?? "failed";
1022
+ lines = data.task?.lines ?? [];
1023
+ attempts = Array.isArray(data.task?.attempts) ? data.task.attempts : void 0;
1024
+ }
1025
+ } catch {}
1026
+ if (status === "done") {
1027
+ if (q.kind === "uninstall") settleDone(q, lines);
1028
+ else finishQueueTask(true, q, lines);
1029
+ } else if (status === "failed") finishQueueTask(false, {
1030
+ ...q,
1031
+ attempts
1032
+ }, lines);
1033
+ else {
1034
+ applyQueue((prev) => prev.filter((x) => x.id !== q.id));
1035
+ maybeStopPoll();
1036
+ }
1037
+ };
1038
+ /** 轮询整个队列:合并服务端 /active(running 在前、pending 在后),
1039
+ * 消失的任务查终态收尾;队列清空即停止轮询。 */
1040
+ const pollQueue = () => {
1041
+ stopPoll();
1042
+ pollRef.current = window.setInterval(async () => {
1043
+ try {
1044
+ const res = await fetch("/dsh-plugin-hub/active", { cache: "no-store" });
1045
+ if (!res.ok) throw new Error(`active ${res.status}`);
1046
+ const data = await res.json();
1047
+ const active = (data.tasks ?? []).filter((a) => typeof a.id === "number");
1048
+ const byId = new Map(active.map((a) => [a.id, a]));
1049
+ const prevQueue = queueRef.current;
1050
+ applyPending(parsePendingRestarts(data.pendingRestarts));
1051
+ applyQueue((prev) => {
1052
+ const next = [];
1053
+ for (const a of active) {
1054
+ const id = a.id;
1055
+ const prevTask = prev.find((q) => q.id === id);
1056
+ const isRemove = a.action === "remove";
1057
+ next.push({
1058
+ id,
1059
+ kind: isRemove ? "uninstall" : "install",
1060
+ target: typeof a.target === "string" ? a.target.replace(/^github:/, "") : prevTask?.target ?? "",
1061
+ desc: prevTask?.desc,
1062
+ repo: prevTask?.repo ?? (isRemove ? null : typeof a.target === "string" ? a.target.replace(/^github:/, "") : null),
1063
+ version: prevTask?.version,
1064
+ updatedAt: prevTask?.updatedAt,
1065
+ status: a.status === "running" ? "running" : "pending",
1066
+ progress: typeof a.progress === "number" ? a.progress : prevTask?.progress ?? 0,
1067
+ lines: Array.isArray(a.lines) ? a.lines : prevTask?.lines ?? [],
1068
+ attempts: Array.isArray(a.attempts) ? a.attempts : prevTask?.attempts ?? []
1069
+ });
1070
+ }
1071
+ const keepOpt = prev.filter((q) => q.optimistic && !next.some((x) => x.id === q.id));
1072
+ if (keepOpt.length > 0) next.push(...keepOpt);
1073
+ const keepGone = prev.filter((q) => !byId.has(q.id) && !next.some((x) => x.id === q.id) && q.status !== "cancelling" && !q.optimistic);
1074
+ if (keepGone.length > 0) next.push(...keepGone);
1075
+ return next;
1076
+ });
1077
+ const gone = prevQueue.filter((q) => !byId.has(q.id) && q.status !== "cancelling" && !q.optimistic && !settlingRef.current.has(q.id));
1078
+ for (const q of gone) {
1079
+ if (!queueRef.current.some((x) => x.id === q.id)) continue;
1080
+ settlingRef.current.add(q.id);
1081
+ try {
1082
+ await settleTask(q);
1083
+ } finally {
1084
+ settlingRef.current.delete(q.id);
1085
+ }
1086
+ }
1087
+ } catch {}
1088
+ }, 600);
1089
+ };
1090
+ (0, react.useEffect)(() => {
1091
+ let cancelled = false;
1092
+ const restore = async () => {
1093
+ try {
1094
+ const res = await fetch("/dsh-plugin-hub/active", { cache: "no-store" });
1095
+ if (!res.ok) return;
1096
+ const data = await res.json();
1097
+ if (cancelled) return;
1098
+ applyPending(parsePendingRestarts(data.pendingRestarts));
1099
+ const items = (data.tasks ?? []).filter((a) => typeof a.id === "number").map((a) => {
1100
+ const isRemove = a.action === "remove";
1101
+ const display = typeof a.displayTarget === "string" ? a.displayTarget.replace(/^github:/, "") : "";
1102
+ const fallback = typeof a.target === "string" ? a.target.replace(/^github:/, "") : "";
1103
+ return {
1104
+ id: a.id,
1105
+ kind: isRemove ? "uninstall" : "install",
1106
+ target: isRemove ? fallback : display || fallback,
1107
+ repo: isRemove ? null : display || fallback,
1108
+ status: a.status === "running" ? "running" : "pending",
1109
+ progress: typeof a.progress === "number" ? a.progress : 0,
1110
+ lines: Array.isArray(a.lines) ? a.lines : [],
1111
+ attempts: Array.isArray(a.attempts) ? a.attempts : []
1112
+ };
1113
+ });
1114
+ if (items.length > 0 || pendingRef.current.length > 0) {
1115
+ applyQueue((prev) => [...prev, ...items.filter((n) => !prev.some((p) => p.id === n.id))]);
1116
+ pollQueue();
1117
+ }
1118
+ } catch {}
1119
+ };
1120
+ restore();
1121
+ return () => {
1122
+ cancelled = true;
1123
+ };
1124
+ }, []);
1125
+ /** 弹窗动作:直接安装。请求宿主本地路由,任务进入服务端队列(FIFO),弹窗内实时显示进度。 */
1126
+ const installNow = async (p, opts) => {
1127
+ const repo = p.source?.repo ?? "";
1128
+ if (!repo) return;
1129
+ const { target } = installTargetOf(p);
1130
+ if (!target) return;
1131
+ if (queueRef.current.some((q) => q.kind === "install" && q.target === repo)) return;
1132
+ if (submittingRef.current.has(repo)) return;
1133
+ submittingRef.current.add(repo);
1134
+ tempIdRef.current -= 1;
1135
+ const tempId = tempIdRef.current;
1136
+ modalTaskRef.current = tempId;
1137
+ applyQueue((prev) => [...prev, {
1138
+ id: tempId,
1139
+ kind: "install",
1140
+ target: repo,
1141
+ repo,
1142
+ desc: p.description,
1143
+ version: p.version,
1144
+ updatedAt: p.dates?.repoUpdatedAt,
1145
+ status: "running",
1146
+ progress: 0,
1147
+ lines: [],
1148
+ optimistic: true,
1149
+ command: installCommandOf(p, true),
1150
+ attempts: []
1151
+ }]);
1152
+ setSubmitting(true);
1153
+ try {
1154
+ const res = await fetch("/dsh-plugin-hub/install", {
1155
+ method: "POST",
1156
+ headers: { "content-type": "application/json" },
1157
+ body: JSON.stringify({
1158
+ repo: target,
1159
+ display: repo,
1160
+ mode: opts?.update ? "update" : void 0
1161
+ })
1162
+ });
1163
+ const data = await res.json();
1164
+ if (!data.ok || typeof data.task !== "number") {
1165
+ applyQueue((prev) => prev.filter((x) => x.id !== tempId));
1166
+ onError(data.error ?? `HTTP ${res.status}`, repo, "install", installCommandOf(p, true), data.attempts);
1167
+ return;
1168
+ }
1169
+ const taskId = data.task;
1170
+ modalTaskRef.current = taskId;
1171
+ applyQueue((prev) => prev.map((x) => x.id === tempId ? {
1172
+ ...x,
1173
+ id: taskId,
1174
+ optimistic: void 0
1175
+ } : x));
1176
+ pendingInfoRef.current.set(repo, {
1177
+ desc: p.description,
1178
+ version: p.version
1179
+ });
1180
+ pollQueue();
1181
+ } catch {
1182
+ applyQueue((prev) => prev.filter((x) => x.id !== tempId));
1183
+ onError(t("installFail"), repo, "install", installCommandOf(p, true));
1184
+ } finally {
1185
+ submittingRef.current.delete(repo);
1186
+ setSubmitting(false);
1187
+ }
1188
+ };
1189
+ /** 弹窗动作:直接卸载。与安装同一队列机制,弹窗内实时显示进度。 */
1190
+ const uninstallNow = async (p) => {
1191
+ const name = installedName(p);
1192
+ if (!name) return;
1193
+ const repo = p.source?.repo ?? null;
1194
+ if (queueRef.current.some((q) => q.kind === "uninstall" && q.target === name)) return;
1195
+ if (submittingRef.current.has(name)) return;
1196
+ submittingRef.current.add(name);
1197
+ setSubmitting(true);
1198
+ try {
1199
+ const res = await fetch("/dsh-plugin-hub/uninstall", {
1200
+ method: "POST",
1201
+ headers: { "content-type": "application/json" },
1202
+ body: JSON.stringify({
1203
+ name,
1204
+ repo: repo ?? void 0
1205
+ })
1206
+ });
1207
+ const data = await res.json();
1208
+ if (!data.ok || typeof data.task !== "number") {
1209
+ onError(data.error ?? `HTTP ${res.status}`, repo, "uninstall");
1210
+ return;
1211
+ }
1212
+ const taskId = data.task;
1213
+ modalTaskRef.current = taskId;
1214
+ applyQueue((prev) => [...prev, {
1215
+ id: taskId,
1216
+ kind: "uninstall",
1217
+ target: name,
1218
+ desc: p.description,
1219
+ repo,
1220
+ status: "pending",
1221
+ progress: 0,
1222
+ lines: []
1223
+ }]);
1224
+ pollQueue();
1225
+ } catch {
1226
+ onError(t("uninstallFail"), repo, "uninstall");
1227
+ } finally {
1228
+ submittingRef.current.delete(name);
1229
+ setSubmitting(false);
1230
+ }
1231
+ };
1232
+ /** 取消任务:排队中立即出队,执行中终止子进程;先标记「正在取消」短暂过渡后再移除。 */
1233
+ const cancelTask = async (id) => {
1234
+ if (modalTaskRef.current === id) modalTaskRef.current = null;
1235
+ applyQueue((prev) => prev.map((q) => q.id === id ? {
1236
+ ...q,
1237
+ status: "cancelling"
1238
+ } : q));
1239
+ try {
1240
+ await fetch("/dsh-plugin-hub/cancel", {
1241
+ method: "POST",
1242
+ headers: { "content-type": "application/json" },
1243
+ body: JSON.stringify({ id })
1244
+ });
1245
+ } catch {}
1246
+ window.setTimeout(() => {
1247
+ applyQueue((prev) => prev.filter((x) => x.id !== id));
1248
+ maybeStopPoll();
1249
+ }, 700);
1250
+ };
1251
+ /** 打开新弹窗前清空任务匹配:避免旧任务 id 误匹配到新弹窗。 */
1252
+ const clearModalTask = () => {
1253
+ modalTaskRef.current = null;
1254
+ };
1255
+ return {
1256
+ queue,
1257
+ pendingRestarts,
1258
+ installModalTask: installPlugin ? queue.find((q) => q.id === modalTaskRef.current && q.status !== "cancelling") ?? queue.find((q) => q.kind === "install" && q.target === (installPlugin.source?.repo ?? "") && q.status !== "cancelling") ?? null : null,
1259
+ uninstallModalTask: uninstallPlugin ? queue.find((q) => q.id === modalTaskRef.current && q.status !== "cancelling") ?? queue.find((q) => q.kind === "uninstall" && q.target === (installedName(uninstallPlugin) ?? "") && q.status !== "cancelling") ?? null : null,
1260
+ submitting,
1261
+ installNow,
1262
+ uninstallNow,
1263
+ cancelTask,
1264
+ clearModalTask
1265
+ };
1266
+ }
1267
+ //#endregion
1268
+ //#region \0dsh-css:src/client/styles/Modal.module.css.mjs
1269
+ const css$3 = ".BiQ1zG_overlay{z-index:998;-webkit-user-select:none;user-select:none;background:#00000061;justify-content:center;align-items:center;animation:.16s ease-out BiQ1zG_overlayIn;display:flex;position:fixed;top:0;bottom:0;left:0;right:0}.BiQ1zG_modal,.BiQ1zG_errorModal{background:var(--hub-bg-1);border:1px solid var(--hub-border-2);border-radius:10px;flex-direction:column;gap:10px;width:420px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px);padding:14px 16px;animation:.18s ease-out BiQ1zG_modalIn;display:flex;box-shadow:0 12px 40px #0000003d}.BiQ1zG_errorModal{width:640px;max-width:calc(100vw - 48px)}.BiQ1zG_modalHead{flex-shrink:0;justify-content:space-between;align-items:center;gap:8px;display:flex}.BiQ1zG_modalTitle{color:var(--hub-text-primary);font-size:14px;font-weight:600;line-height:20px}.BiQ1zG_modalTitleBusy{color:var(--hub-brand)}.BiQ1zG_modalTitleQueued{color:var(--hub-warning)}.BiQ1zG_modalClose{width:24px;height:24px;color:var(--hub-text-tertiary);cursor:pointer;-webkit-user-select:none;user-select:none;background:0 0;border:none;border-radius:6px;flex-shrink:0;justify-content:center;align-items:center;font-size:16px;line-height:1;display:inline-flex}.BiQ1zG_modalClose:hover{color:var(--hub-text-primary);background:var(--hub-bg-hover)}.BiQ1zG_modalCloseIcon{flex-shrink:0;width:14px;height:14px}.BiQ1zG_modalDesc{color:var(--hub-text-secondary);font-size:12px;line-height:18px}.BiQ1zG_trustHint{color:var(--hub-danger-text);background:var(--hub-danger-tint);border:1px solid var(--hub-danger-border-soft);border-radius:6px;padding:6px 10px;font-size:12px;font-weight:500;line-height:18px}.BiQ1zG_modalRow{align-items:baseline;gap:8px;min-width:0;font-size:12px;line-height:18px;display:flex}.BiQ1zG_modalLabel{min-width:64px;color:var(--hub-text-tertiary);flex-shrink:0}.BiQ1zG_modalValue{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--hub-text-primary);font-weight:500;overflow:hidden}.BiQ1zG_modalLink{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--hub-brand);cursor:pointer;-webkit-user-select:none;user-select:none;align-items:center;gap:4px;font-weight:500;text-decoration:none;transition:color .12s;display:inline-flex;overflow:hidden}.BiQ1zG_modalLink:hover{color:var(--hub-brand-hover);text-decoration:underline}.BiQ1zG_linkIcon{flex-shrink:0}.BiQ1zG_modalCmd{color:var(--hub-text-primary);background:var(--hub-bg-2);border:1px solid var(--hub-border-2);cursor:pointer;-webkit-user-select:none;user-select:none;border-radius:6px;justify-content:space-between;align-items:center;gap:8px;padding:6px 6px 6px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:18px;transition:border-color .12s,background .12s;display:flex}.BiQ1zG_modalCmd:hover{border-color:var(--hub-border-ghost);background:var(--hub-bg-3)}.BiQ1zG_modalCmdText{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.BiQ1zG_modalCmdCopy{color:var(--hub-brand);cursor:pointer;-webkit-user-select:none;user-select:none;background:0 0;border:none;border-radius:4px;flex-shrink:0;align-items:center;gap:3px;padding:1px 7px;font-family:inherit;font-size:11px;line-height:16px;transition:color .12s,background .12s;display:inline-flex}.BiQ1zG_modalCmdCopy:hover{color:var(--hub-brand-hover);background:var(--hub-bg-2)}.BiQ1zG_modalActions{justify-content:flex-end;align-items:center;gap:8px;margin-top:2px;display:flex}.BiQ1zG_modalBody{flex-direction:column;flex:auto;gap:10px;min-height:0;display:flex;overflow-y:auto}.BiQ1zG_toast{z-index:1000;background:var(--hub-btn-fill);color:var(--hub-text-on-fill);pointer-events:none;white-space:nowrap;border:none;border-radius:8px;padding:10px 16px;font-size:12px;font-weight:500;line-height:18px;animation:.22s ease-out BiQ1zG_toastIn;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);box-shadow:0 6px 24px #00000047}@keyframes BiQ1zG_toastIn{0%{opacity:0;transform:translate(-50%,-44%)}to{opacity:1;transform:translate(-50%,-50%)}}.BiQ1zG_toastFail{background:var(--hub-danger);color:#fff;border-color:#ffffff3d}.BiQ1zG_queueSection{flex-direction:column;gap:6px;display:flex}.BiQ1zG_queueSectionTitle{color:var(--hub-text-secondary);padding:4px 2px 0;font-size:12px;font-weight:600;line-height:18px}.BiQ1zG_pendingRowStatus{color:var(--hub-warn);flex-shrink:0;font-weight:500}.BiQ1zG_pendingRowActions{flex-shrink:0;gap:8px;margin-left:auto;display:flex}.BiQ1zG_queueRow{border:1px solid var(--hub-border-2);background:var(--hub-bg-2);cursor:pointer;-webkit-user-select:none;user-select:none;border-radius:8px;flex-direction:column;gap:5px;padding:8px 10px;font-size:12px;line-height:18px;transition:background .12s;display:flex}.BiQ1zG_queueRow:hover{background:var(--hub-brand-tint)}.BiQ1zG_queueRowHead{align-items:center;gap:10px;display:flex}.BiQ1zG_queueRowTarget{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--hub-text-primary);flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden}.BiQ1zG_queueRowDesc{color:var(--hub-text-tertiary);-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.BiQ1zG_queueRowBody{align-items:center;gap:8px;display:flex}.BiQ1zG_queueRowStatus{color:var(--hub-brand);flex-shrink:0;font-weight:500}.BiQ1zG_queueRowTrack{flex:1;min-width:0}.BiQ1zG_queueRowPct{color:var(--hub-text-tertiary);font-variant-numeric:tabular-nums;flex-shrink:0}.BiQ1zG_stripCancel{color:var(--hub-text-secondary);cursor:pointer;-webkit-user-select:none;user-select:none;background:0 0;border:none;border-radius:6px;flex-shrink:0;padding:2px 10px;font-size:12px;line-height:18px;transition:background .12s,color .12s}.BiQ1zG_stripCancel:hover{background:var(--hub-danger-tint);color:var(--hub-danger-text)}.BiQ1zG_stripCancel:disabled{opacity:.45;cursor:default}.BiQ1zG_queuedHint{color:var(--hub-brand);background:var(--hub-brand-tint);border:1px solid var(--hub-brand-border-soft);border-radius:6px;padding:5px 10px;font-size:12px;line-height:18px}.BiQ1zG_errorTitle{color:var(--hub-danger-text);font-size:14px;font-weight:600;line-height:20px}.BiQ1zG_errorBox{border:1px solid var(--hub-danger-border);background:var(--hub-danger-tint-weak);max-height:240px;color:var(--hub-text-primary);white-space:pre-wrap;word-break:break-word;-webkit-user-select:none;user-select:none;border-radius:6px;margin:0;padding:10px 12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:17px;overflow:auto}.BiQ1zG_errorCopySoft{color:var(--hub-text-tertiary);cursor:pointer;-webkit-user-select:none;user-select:none;background:0 0;border:none;border-radius:4px;flex-shrink:0;align-items:center;padding:2px 8px;font-size:11px;line-height:16px;transition:color .12s,background .12s;display:inline-flex}.BiQ1zG_errorCopySoft:hover{color:var(--hub-text-secondary);background:var(--hub-bg-2)}.BiQ1zG_errorHint{color:var(--hub-text-tertiary);font-size:12px;line-height:18px}.BiQ1zG_modalCancel,.BiQ1zG_modalCopy{color:var(--hub-text-secondary);cursor:pointer;-webkit-user-select:none;user-select:none;background:0 0;border:none;border-radius:6px;padding:5px 14px;font-size:12px;line-height:18px;transition:color .12s,background .12s}.BiQ1zG_modalCancel:hover,.BiQ1zG_modalCopy:hover{color:var(--hub-text-primary);background:var(--hub-bg-hover)}.BiQ1zG_modalInstall{color:var(--hub-text-on-fill);background:var(--hub-btn-fill);cursor:pointer;-webkit-user-select:none;user-select:none;border:none;border-radius:6px;padding:5px 14px;font-size:12px;font-weight:600;line-height:18px;transition:background .12s}.BiQ1zG_modalInstall:hover{background:var(--hub-btn-hover)}.BiQ1zG_modalCopy:disabled,.BiQ1zG_modalInstall:disabled,.BiQ1zG_modalCancel:disabled,.BiQ1zG_modalClose:disabled,.BiQ1zG_uninstallConfirm:disabled{opacity:.55;cursor:not-allowed;pointer-events:none}.BiQ1zG_uninstallConfirm{color:#fff;background:var(--hub-danger);cursor:pointer;-webkit-user-select:none;user-select:none;border:none;border-radius:6px;padding:5px 14px;font-size:12px;font-weight:600;line-height:18px;transition:background .12s}.BiQ1zG_uninstallConfirm:hover{background:var(--hub-danger-hover)}.BiQ1zG_result{text-align:center;flex-direction:column;align-items:center;gap:5px;padding:10px 0 2px;display:flex}.BiQ1zG_resultCheck{background:var(--hub-success-tint);width:40px;height:40px;color:var(--hub-success);border-radius:50%;justify-content:center;align-items:center;margin-bottom:3px;display:inline-flex}.BiQ1zG_resultCheckIcon{flex-shrink:0;width:20px;height:20px}.BiQ1zG_resultTitle{color:var(--hub-text-primary);font-size:13px;font-weight:600;line-height:20px}.BiQ1zG_resultDesc{color:var(--hub-text-secondary);font-size:12px;line-height:18px}.BiQ1zG_resultRestarting{color:var(--hub-text-tertiary);padding:12px 0 6px;font-size:12px;line-height:18px}.BiQ1zG_restartLater{color:var(--hub-text-secondary);cursor:pointer;-webkit-user-select:none;user-select:none;background:0 0;border:none;border-radius:6px;padding:5px 14px;font-size:12px;line-height:18px;transition:color .12s,background .12s}.BiQ1zG_restartLater:hover{color:var(--hub-text-primary);background:var(--hub-bg-hover)}.BiQ1zG_restartNow{color:var(--hub-text-on-fill);background:var(--hub-btn-fill);cursor:pointer;-webkit-user-select:none;user-select:none;border:none;border-radius:6px;padding:5px 14px;font-size:12px;font-weight:600;line-height:18px;transition:background .12s}.BiQ1zG_restartNow:hover{background:var(--hub-btn-hover)}.BiQ1zG_restartNow:disabled{opacity:.55;cursor:not-allowed;pointer-events:none}.BiQ1zG_result .BiQ1zG_modalActions{justify-content:center;gap:10px;margin-top:0;padding:6px 0 2px}.BiQ1zG_result .BiQ1zG_modalActions .BiQ1zG_restartLater,.BiQ1zG_result .BiQ1zG_modalActions .BiQ1zG_restartNow{min-width:100px}.BiQ1zG_progress{margin:10px 0 2px}.BiQ1zG_progressHead{justify-content:flex-end;align-items:center;margin-bottom:3px;display:flex}.BiQ1zG_progressText{color:var(--hub-text-secondary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:14px}.BiQ1zG_progressTrack{background:var(--hub-border-2);border-radius:2px;height:4px;overflow:hidden}.BiQ1zG_progressFill{background:var(--hub-brand);border-radius:2px;height:100%;transition:width .32s}.BiQ1zG_progressFillFail{background:var(--hub-danger)}@keyframes BiQ1zG_overlayIn{0%{opacity:0}to{opacity:1}}@keyframes BiQ1zG_modalIn{0%{opacity:0;transform:translateY(6px)scale(.98)}to{opacity:1;transform:translateY(0)scale(1)}}.BiQ1zG_noticeList{flex-direction:column;gap:10px;padding-right:2px;display:flex}.BiQ1zG_noticeRow{border:1px solid var(--hub-border-2);background:var(--hub-bg-2);border-radius:8px;flex-direction:column;align-items:stretch;gap:6px;padding:10px 12px;display:flex}.BiQ1zG_noticeRowMain{align-items:center;gap:10px;width:100%;min-width:0;display:flex}.BiQ1zG_noticeBadgeOk,.BiQ1zG_noticeBadgeFail{border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;width:28px;height:28px;display:inline-flex;box-shadow:0 2px 6px #0000002e}.BiQ1zG_noticeBadgeOk{background:var(--hub-success)}.BiQ1zG_noticeBadgeFail{background:var(--hub-danger)}.BiQ1zG_noticeBadgeIcon{flex-shrink:0;width:12px;height:12px}.BiQ1zG_noticeMain{flex-direction:column;flex:1;gap:8px;min-width:0;display:flex}.BiQ1zG_noticeHead{align-items:center;gap:8px;min-width:0;display:flex}.BiQ1zG_noticeTextOk{color:var(--hub-success);flex-shrink:0;font-size:12px;font-weight:600;line-height:18px}.BiQ1zG_noticeTextFail{color:var(--hub-danger-text);flex-shrink:0;font-size:12px;font-weight:600;line-height:18px}.BiQ1zG_failList{flex-direction:column;gap:12px;padding-right:2px;display:flex}.BiQ1zG_failRow{border:1px solid var(--hub-border-2);background:var(--hub-bg-2);border-radius:8px;flex-direction:column;gap:8px;padding:10px 12px;display:flex}.BiQ1zG_failHead{align-items:center;gap:8px;min-width:0;display:flex}.BiQ1zG_failKind{border:1px solid #0000;border-radius:4px;flex-shrink:0;padding:0 6px;font-size:10px;font-weight:500;line-height:16px}.BiQ1zG_failKindInstall{color:var(--hub-danger-text);border-color:var(--hub-danger-border);background:var(--hub-danger-tint)}.BiQ1zG_failKindUninstall{color:var(--hub-warn);border-color:var(--hub-warn-border);background:var(--hub-warn-tint)}.BiQ1zG_failRepo{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--hub-brand);cursor:pointer;-webkit-user-select:none;user-select:none;flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:18px;text-decoration:none;overflow:hidden}.BiQ1zG_failRepo:hover{text-decoration:underline}.BiQ1zG_noticeFoot{justify-content:flex-end;align-items:center;display:flex}.BiQ1zG_noticeTime{color:var(--hub-text-tertiary);font-variant-numeric:tabular-nums;flex-shrink:0;font-size:11px;line-height:16px}.BiQ1zG_noticeRemove{width:22px;height:22px;color:var(--hub-text-tertiary);cursor:pointer;-webkit-user-select:none;user-select:none;background:0 0;border:none;border-radius:5px;flex-shrink:0;justify-content:center;align-items:center;margin-left:auto;transition:color .12s,background .12s;display:inline-flex}.BiQ1zG_noticeRemove:hover{color:var(--hub-danger-text);background:var(--hub-danger-tint)}.BiQ1zG_failCopy{color:var(--hub-text-tertiary);cursor:pointer;-webkit-user-select:none;user-select:none;background:0 0;border:none;border-radius:4px;flex-shrink:0;align-items:center;padding:2px 8px;font-size:11px;line-height:16px;transition:color .12s,background .12s;display:inline-flex}.BiQ1zG_failCopy:hover{color:var(--hub-text-secondary);background:var(--hub-bg-2)}.BiQ1zG_failEmpty{text-align:center;color:var(--hub-text-tertiary);padding:24px 0;font-size:12px;line-height:18px}.BiQ1zG_failClear{color:var(--hub-danger);cursor:pointer;-webkit-user-select:none;user-select:none;background:0 0;border:none;border-radius:6px;padding:5px 14px;font-size:12px;line-height:18px;transition:color .12s,background .12s}.BiQ1zG_failClear:hover{color:#fff;background:var(--hub-danger)}.BiQ1zG_failBigIssue{box-sizing:border-box;text-align:center;color:#fff;background:var(--hub-danger);border:1px solid var(--hub-danger);cursor:pointer;-webkit-user-select:none;user-select:none;border-radius:8px;width:100%;margin-top:4px;padding:3px 16px;font-size:13px;font-weight:600;line-height:18px;text-decoration:none;transition:background .12s,border-color .12s,box-shadow .12s;display:block}.BiQ1zG_failBigIssue:hover{background:var(--hub-danger-hover);border-color:var(--hub-danger-hover);box-shadow:0 3px 10px #d1242f59}.BiQ1zG_failPrepareHint{color:var(--hub-warning);background:var(--hub-warning-tint);border:1px solid var(--hub-warning-border);border-radius:6px;margin-top:6px;padding:8px 12px;font-size:12px;line-height:18px}";
1270
+ const tagId$3 = "dsh-plugin/Modal.module.css";
1271
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
1272
+ const tag = document.createElement("style");
1273
+ tag.dataset.plugin = "dsh-plugin";
1274
+ tag.dataset.pluginCss = tagId$3;
1275
+ tag.textContent = css$3;
1276
+ document.head.appendChild(tag);
1277
+ }
1278
+ var Modal_module_css_default = {
1279
+ "queueRowTarget": "BiQ1zG_queueRowTarget",
1280
+ "failClear": "BiQ1zG_failClear",
1281
+ "modalValue": "BiQ1zG_modalValue",
1282
+ "modalTitleQueued": "BiQ1zG_modalTitleQueued",
1283
+ "overlay": "BiQ1zG_overlay",
1284
+ "noticeRow": "BiQ1zG_noticeRow",
1285
+ "toast": "BiQ1zG_toast",
1286
+ "restartLater": "BiQ1zG_restartLater",
1287
+ "resultCheckIcon": "BiQ1zG_resultCheckIcon",
1288
+ "linkIcon": "BiQ1zG_linkIcon",
1289
+ "failKindUninstall": "BiQ1zG_failKindUninstall",
1290
+ "failRepo": "BiQ1zG_failRepo",
1291
+ "modalCmd": "BiQ1zG_modalCmd",
1292
+ "modal": "BiQ1zG_modal",
1293
+ "queueRowPct": "BiQ1zG_queueRowPct",
1294
+ "failKind": "BiQ1zG_failKind",
1295
+ "modalTitle": "BiQ1zG_modalTitle",
1296
+ "modalTitleBusy": "BiQ1zG_modalTitleBusy",
1297
+ "modalBody": "BiQ1zG_modalBody",
1298
+ "modalCloseIcon": "BiQ1zG_modalCloseIcon",
1299
+ "errorCopySoft": "BiQ1zG_errorCopySoft",
1300
+ "modalCancel": "BiQ1zG_modalCancel",
1301
+ "overlayIn": "BiQ1zG_overlayIn",
1302
+ "modalClose": "BiQ1zG_modalClose",
1303
+ "modalCopy": "BiQ1zG_modalCopy",
1304
+ "modalCmdText": "BiQ1zG_modalCmdText",
1305
+ "queueSection": "BiQ1zG_queueSection",
1306
+ "queueRow": "BiQ1zG_queueRow",
1307
+ "errorHint": "BiQ1zG_errorHint",
1308
+ "noticeMain": "BiQ1zG_noticeMain",
1309
+ "noticeFoot": "BiQ1zG_noticeFoot",
1310
+ "failCopy": "BiQ1zG_failCopy",
1311
+ "failBigIssue": "BiQ1zG_failBigIssue",
1312
+ "modalRow": "BiQ1zG_modalRow",
1313
+ "queueRowBody": "BiQ1zG_queueRowBody",
1314
+ "queueRowTrack": "BiQ1zG_queueRowTrack",
1315
+ "errorBox": "BiQ1zG_errorBox",
1316
+ "progressHead": "BiQ1zG_progressHead",
1317
+ "noticeHead": "BiQ1zG_noticeHead",
1318
+ "progressFillFail": "BiQ1zG_progressFillFail",
1319
+ "noticeTextFail": "BiQ1zG_noticeTextFail",
1320
+ "noticeRemove": "BiQ1zG_noticeRemove",
1321
+ "modalLabel": "BiQ1zG_modalLabel",
1322
+ "pendingRowActions": "BiQ1zG_pendingRowActions",
1323
+ "resultTitle": "BiQ1zG_resultTitle",
1324
+ "restartNow": "BiQ1zG_restartNow",
1325
+ "noticeBadgeIcon": "BiQ1zG_noticeBadgeIcon",
1326
+ "failPrepareHint": "BiQ1zG_failPrepareHint",
1327
+ "noticeBadgeOk": "BiQ1zG_noticeBadgeOk",
1328
+ "failEmpty": "BiQ1zG_failEmpty",
1329
+ "result": "BiQ1zG_result",
1330
+ "modalInstall": "BiQ1zG_modalInstall",
1331
+ "noticeTextOk": "BiQ1zG_noticeTextOk",
1332
+ "noticeList": "BiQ1zG_noticeList",
1333
+ "queueSectionTitle": "BiQ1zG_queueSectionTitle",
1334
+ "stripCancel": "BiQ1zG_stripCancel",
1335
+ "progress": "BiQ1zG_progress",
1336
+ "modalDesc": "BiQ1zG_modalDesc",
1337
+ "modalIn": "BiQ1zG_modalIn",
1338
+ "errorTitle": "BiQ1zG_errorTitle",
1339
+ "failList": "BiQ1zG_failList",
1340
+ "noticeTime": "BiQ1zG_noticeTime",
1341
+ "queuedHint": "BiQ1zG_queuedHint",
1342
+ "resultDesc": "BiQ1zG_resultDesc",
1343
+ "trustHint": "BiQ1zG_trustHint",
1344
+ "noticeRowMain": "BiQ1zG_noticeRowMain",
1345
+ "queueRowDesc": "BiQ1zG_queueRowDesc",
1346
+ "modalHead": "BiQ1zG_modalHead",
1347
+ "queueRowStatus": "BiQ1zG_queueRowStatus",
1348
+ "uninstallConfirm": "BiQ1zG_uninstallConfirm",
1349
+ "failKindInstall": "BiQ1zG_failKindInstall",
1350
+ "toastFail": "BiQ1zG_toastFail",
1351
+ "queueRowHead": "BiQ1zG_queueRowHead",
1352
+ "resultRestarting": "BiQ1zG_resultRestarting",
1353
+ "resultCheck": "BiQ1zG_resultCheck",
1354
+ "failRow": "BiQ1zG_failRow",
1355
+ "pendingRowStatus": "BiQ1zG_pendingRowStatus",
1356
+ "failHead": "BiQ1zG_failHead",
1357
+ "progressFill": "BiQ1zG_progressFill",
1358
+ "progressTrack": "BiQ1zG_progressTrack",
1359
+ "modalActions": "BiQ1zG_modalActions",
1360
+ "noticeBadgeFail": "BiQ1zG_noticeBadgeFail",
1361
+ "modalLink": "BiQ1zG_modalLink",
1362
+ "errorModal": "BiQ1zG_errorModal",
1363
+ "progressText": "BiQ1zG_progressText",
1364
+ "modalCmdCopy": "BiQ1zG_modalCmdCopy",
1365
+ "toastIn": "BiQ1zG_toastIn"
1366
+ };
1367
+ //#endregion
1368
+ //#region src/client/components/icons.tsx
1369
+ /**
1370
+ * Inline SVG icons used across the client. Icons inherit currentColor so
1371
+ * they adapt to themes and disabled states automatically.
1372
+ */
1373
+ /** 弹窗右上角关闭按钮图标:内联 SVG 十字(stroke 继承 currentColor),随按钮禁用态一起变淡。 */
1374
+ function CloseIcon() {
1375
+ return (0, react.createElement)("svg", {
1376
+ className: Modal_module_css_default.modalCloseIcon,
1377
+ viewBox: "0 0 16 16",
1378
+ width: 14,
1379
+ height: 14,
1380
+ fill: "none",
1381
+ "aria-hidden": "true"
1382
+ }, (0, react.createElement)("path", {
1383
+ d: "M3 3l10 10M13 3L3 13",
1384
+ stroke: "currentColor",
1385
+ strokeWidth: 1.6,
1386
+ strokeLinecap: "round"
1387
+ }));
1388
+ }
1389
+ /** 来源链接图标:链节(stroke 继承 currentColor),弹窗里官网收录地址旁提示「可点击跳转」。 */
1390
+ function LinkIcon() {
1391
+ return (0, react.createElement)("svg", {
1392
+ className: Modal_module_css_default.linkIcon,
1393
+ viewBox: "0 0 24 24",
1394
+ width: 13,
1395
+ height: 13,
1396
+ fill: "none",
1397
+ "aria-hidden": "true"
1398
+ }, (0, react.createElement)("path", {
1399
+ d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71",
1400
+ stroke: "currentColor",
1401
+ strokeWidth: 1.8,
1402
+ strokeLinecap: "round",
1403
+ strokeLinejoin: "round"
1404
+ }), (0, react.createElement)("path", {
1405
+ d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71",
1406
+ stroke: "currentColor",
1407
+ strokeWidth: 1.8,
1408
+ strokeLinecap: "round",
1409
+ strokeLinejoin: "round"
1410
+ }));
1411
+ }
1412
+ /** 品牌 logo 图标:蓝紫渐变圆角方块 + 白色拼图块(2x2 错落),标题左侧的品牌标识。 */
1413
+ function LogoIcon() {
1414
+ return (0, react.createElement)("svg", {
1415
+ className: Header_module_css_default.logoIcon,
1416
+ viewBox: "0 0 24 24",
1417
+ width: 20,
1418
+ height: 20,
1419
+ "aria-hidden": "true"
1420
+ }, (0, react.createElement)("defs", null, (0, react.createElement)("linearGradient", {
1421
+ id: "dshLogoGrad",
1422
+ x1: "0",
1423
+ y1: "0",
1424
+ x2: "1",
1425
+ y2: "1"
1426
+ }, (0, react.createElement)("stop", {
1427
+ offset: "0%",
1428
+ stopColor: "#4f46e5"
1429
+ }), (0, react.createElement)("stop", {
1430
+ offset: "100%",
1431
+ stopColor: "#7c3aed"
1432
+ }))), (0, react.createElement)("rect", {
1433
+ x: 1,
1434
+ y: 1,
1435
+ width: 22,
1436
+ height: 22,
1437
+ rx: 6,
1438
+ fill: "url(#dshLogoGrad)"
1439
+ }), (0, react.createElement)("rect", {
1440
+ x: 5.5,
1441
+ y: 5.5,
1442
+ width: 5.5,
1443
+ height: 5.5,
1444
+ rx: 1.5,
1445
+ fill: "#ffffff"
1446
+ }), (0, react.createElement)("rect", {
1447
+ x: 13,
1448
+ y: 5.5,
1449
+ width: 5.5,
1450
+ height: 5.5,
1451
+ rx: 1.5,
1452
+ fill: "#ffffff",
1453
+ opacity: .55
1454
+ }), (0, react.createElement)("rect", {
1455
+ x: 5.5,
1456
+ y: 13,
1457
+ width: 5.5,
1458
+ height: 5.5,
1459
+ rx: 1.5,
1460
+ fill: "#ffffff",
1461
+ opacity: .55
1462
+ }), (0, react.createElement)("rect", {
1463
+ x: 13,
1464
+ y: 13,
1465
+ width: 5.5,
1466
+ height: 5.5,
1467
+ rx: 1.5,
1468
+ fill: "#ffffff"
1469
+ }));
1470
+ }
1471
+ /** GitHub 图标:官方 GitHub Mark(octocat),fill 继承 currentColor,头部右上角源码链接用。 */
1472
+ function GitHubIcon() {
1473
+ return (0, react.createElement)("svg", {
1474
+ className: Header_module_css_default.githubIcon,
1475
+ viewBox: "0 0 24 24",
1476
+ width: 18,
1477
+ height: 18,
1478
+ fill: "currentColor",
1479
+ "aria-hidden": "true"
1480
+ }, (0, react.createElement)("path", { d: "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" }));
1481
+ }
1482
+ /** 复制图标:双层矩形(stroke 继承 currentColor),手动命令旁的复制按钮用。 */
1483
+ function CopyIcon() {
1484
+ return (0, react.createElement)("svg", {
1485
+ className: Header_module_css_default.copyIcon,
1486
+ viewBox: "0 0 16 16",
1487
+ width: 12,
1488
+ height: 12,
1489
+ fill: "none",
1490
+ "aria-hidden": "true"
1491
+ }, (0, react.createElement)("rect", {
1492
+ x: 5.5,
1493
+ y: 5.5,
1494
+ width: 7,
1495
+ height: 7,
1496
+ rx: 1.5,
1497
+ stroke: "currentColor",
1498
+ strokeWidth: 1.4
1499
+ }), (0, react.createElement)("path", {
1500
+ d: "M10.5 5.5v-2a1.5 1.5 0 0 0-1.5-1.5H4.5A1.5 1.5 0 0 0 3 3.5v4A1.5 1.5 0 0 0 4.5 9h1",
1501
+ stroke: "currentColor",
1502
+ strokeWidth: 1.4,
1503
+ strokeLinecap: "round"
1504
+ }));
1505
+ }
1506
+ /** 下拉箭头图标:向下 V 形(stroke 继承 currentColor),下拉框触发器右侧提示可展开。 */
1507
+ function ChevronDownIcon() {
1508
+ return (0, react.createElement)("svg", {
1509
+ viewBox: "0 0 16 16",
1510
+ width: 12,
1511
+ height: 12,
1512
+ fill: "none",
1513
+ "aria-hidden": "true"
1514
+ }, (0, react.createElement)("path", {
1515
+ d: "M3.5 5.5L8 10l4.5-4.5",
1516
+ stroke: "currentColor",
1517
+ strokeWidth: 1.6,
1518
+ strokeLinecap: "round",
1519
+ strokeLinejoin: "round"
1520
+ }));
1521
+ }
1522
+ //#endregion
1523
+ //#region src/client/components/ProgressView.tsx
1524
+ /**
1525
+ * Inline progress block shared by the install and uninstall dialogs:
1526
+ * a real percentage bar (server-estimated, client-polled). No live terminal
1527
+ * window — pnpm emits nothing during git clone/build, so a log pane would be
1528
+ * empty for most of the run; failures surface their full output in ErrorModal.
1529
+ */
1530
+ function ProgressView({ task }) {
1531
+ return (0, react.createElement)("div", { className: Modal_module_css_default.progress }, (0, react.createElement)("div", { className: Modal_module_css_default.progressHead }, (0, react.createElement)("span", { className: Modal_module_css_default.progressText }, `${Math.round(task.progress)}%`)), (0, react.createElement)("div", { className: Modal_module_css_default.progressTrack }, (0, react.createElement)("div", {
1532
+ className: task.status === "failed" ? `${Modal_module_css_default.progressFill} ${Modal_module_css_default.progressFillFail}` : Modal_module_css_default.progressFill,
1533
+ style: { width: `${task.progress}%` }
1534
+ })));
1535
+ }
1536
+ //#endregion
1537
+ //#region src/client/components/modals.tsx
1538
+ /**
1539
+ * Dialog layer for the Plugin Hub: the install-confirm dialog, the uninstall
1540
+ * dialog and the global toast. Both dialogs lock themselves while a mutation
1541
+ * or restart is running, then switch to a result view offering an immediate
1542
+ * restart or a "later" deferral.
1543
+ */
1544
+ /** 完成结果视图:绿色对勾 + 标题/描述 + 「稍后重启 / 立即重启」按钮对(部分插件需重启后才会挂载) */
1545
+ function ResultView({ title, desc, t, restarting, onRestart, onClose }) {
1546
+ return (0, react.createElement)("div", { className: Modal_module_css_default.result }, (0, react.createElement)("div", { className: Modal_module_css_default.resultCheck }, (0, react.createElement)("svg", {
1547
+ className: Modal_module_css_default.resultCheckIcon,
1548
+ viewBox: "0 0 16 16",
1549
+ width: 20,
1550
+ height: 20,
1551
+ fill: "none",
1552
+ "aria-hidden": "true"
1553
+ }, (0, react.createElement)("path", {
1554
+ d: "M2.5 8.5l3.5 3.5 7.5-7.5",
1555
+ stroke: "currentColor",
1556
+ strokeWidth: 2,
1557
+ strokeLinecap: "round",
1558
+ strokeLinejoin: "round"
1559
+ }))), (0, react.createElement)("div", { className: Modal_module_css_default.resultTitle }, title), desc ? (0, react.createElement)("div", { className: Modal_module_css_default.resultDesc }, desc) : null, (0, react.createElement)("div", { className: Modal_module_css_default.resultRestarting }, restarting ? t("restarting") : t("restartHint")), (0, react.createElement)("div", { className: Modal_module_css_default.modalActions }, (0, react.createElement)("button", {
1560
+ className: Modal_module_css_default.restartLater,
1561
+ onClick: onClose,
1562
+ disabled: restarting
1563
+ }, t("restartLater")), (0, react.createElement)("button", {
1564
+ className: Modal_module_css_default.restartNow,
1565
+ onClick: onRestart,
1566
+ disabled: restarting
1567
+ }, restarting ? t("restarting") : t("restartNow"))));
1568
+ }
1569
+ /**
1570
+ * 信任确认弹窗:安装进入后台队列后弹窗仍可关闭(任务继续),
1571
+ * 只在本任务执行中展示实时进度;完成后切换为结果视图,与卸载一致。
1572
+ */
1573
+ function InstallModal(props) {
1574
+ const { plugin, done, task, t, langPath, restarting, submitting, update, onClose, onCopy, onInstall, onRestart } = props;
1575
+ const busy = submitting || task !== null && (task.status === "pending" || task.status === "running");
1576
+ const name = plugin.displayName ?? plugin.slug;
1577
+ const busyTitle = (label) => langPath === "zh/" ? `${name} 插件${label}` : `${label} ${name}`;
1578
+ const title = busy ? task && task.status === "pending" ? busyTitle(update ? t("queuedUpdateTitle") : t("queuedTitle")) : busyTitle(update ? t("updating") : t("installing")) : done ? update ? t("updateResultTitle") : t("installResultTitle") : update ? t("confirmUpdateTitle") : t("confirmTitle");
1579
+ return (0, react.createElement)("div", {
1580
+ className: Modal_module_css_default.overlay,
1581
+ onClick: (e) => {
1582
+ if (e.target === e.currentTarget) onClose();
1583
+ }
1584
+ }, (0, react.createElement)("div", {
1585
+ className: Modal_module_css_default.modal,
1586
+ role: "dialog",
1587
+ "aria-modal": "true"
1588
+ }, (0, react.createElement)("div", { className: Modal_module_css_default.modalHead }, (0, react.createElement)("div", { className: busy ? `${Modal_module_css_default.modalTitle} ${task && task.status === "pending" ? Modal_module_css_default.modalTitleQueued : Modal_module_css_default.modalTitleBusy}` : Modal_module_css_default.modalTitle }, title), (0, react.createElement)("button", {
1589
+ className: Modal_module_css_default.modalClose,
1590
+ "aria-label": t("confirmCancel"),
1591
+ onClick: () => onClose()
1592
+ }, (0, react.createElement)(CloseIcon))), done ? (0, react.createElement)(ResultView, {
1593
+ title: update ? t("updateResultTitle") : t("installResultTitle"),
1594
+ desc: update ? t("updateResultDesc") : t("installResultDesc"),
1595
+ t,
1596
+ restarting,
1597
+ onRestart,
1598
+ onClose
1599
+ }) : (0, react.createElement)("div", { className: Modal_module_css_default.modalBody }, (0, react.createElement)("div", { className: Modal_module_css_default.trustHint }, t("confirmDesc")), (0, react.createElement)("div", { className: Modal_module_css_default.modalRow }, (0, react.createElement)("span", { className: Modal_module_css_default.modalLabel }, t("confirmPlugin")), (0, react.createElement)("span", {
1600
+ className: Modal_module_css_default.modalValue,
1601
+ title: plugin.displayName ?? plugin.slug
1602
+ }, plugin.displayName ?? plugin.slug)), plugin.source?.repo ? (0, react.createElement)("div", { className: Modal_module_css_default.modalRow }, (0, react.createElement)("span", { className: Modal_module_css_default.modalLabel }, t("confirmSource")), (0, react.createElement)("a", {
1603
+ className: Modal_module_css_default.modalLink,
1604
+ href: pluginDetailUrl(plugin, langPath),
1605
+ target: "_blank",
1606
+ rel: "noopener noreferrer",
1607
+ title: plugin.source.repo
1608
+ }, (0, react.createElement)(LinkIcon), plugin.source.repo)) : null, (0, react.createElement)("div", {
1609
+ className: Modal_module_css_default.modalCmd,
1610
+ onClick: onCopy,
1611
+ title: t("copyInstallCommand"),
1612
+ role: "button",
1613
+ tabIndex: 0
1614
+ }, (0, react.createElement)("span", { className: Modal_module_css_default.modalCmdText }, installCommandOf(plugin)), (0, react.createElement)("span", { className: Modal_module_css_default.modalCmdCopy }, (0, react.createElement)(CopyIcon), t("copyCmdLabel"))), task && task.status === "pending" ? (0, react.createElement)("div", { className: Modal_module_css_default.queuedHint }, t("queuedHint")) : null, task ? (0, react.createElement)(ProgressView, { task }) : null, (0, react.createElement)("div", { className: Modal_module_css_default.modalActions }, (0, react.createElement)("button", {
1615
+ className: Modal_module_css_default.modalCopy,
1616
+ disabled: busy,
1617
+ onClick: onCopy
1618
+ }, t("copyInstallCommand")), (0, react.createElement)("button", {
1619
+ className: Modal_module_css_default.modalInstall,
1620
+ disabled: busy,
1621
+ onClick: onInstall
1622
+ }, busy ? task && task.status === "pending" ? update ? t("queuedUpdateTitle") : t("queuedTitle") : update ? t("updating") : t("installing") : update ? t("updateNow") : t("installNow"))))));
1623
+ }
1624
+ /** 卸载确认弹窗:确认/进行中(后台队列,可关闭);完成后切换为结果视图(成功即生效,仅「完成」关闭)。 */
1625
+ function UninstallModal(props) {
1626
+ const { plugin, done, task, t, langPath, restarting, submitting, onClose, onCancel, onCopyCommand, onConfirm, onRestart } = props;
1627
+ const busy = submitting || task !== null && (task.status === "pending" || task.status === "running");
1628
+ const name = plugin.displayName ?? plugin.slug;
1629
+ const busyTitle = (label) => langPath === "zh/" ? `${name} 插件${label}` : `${label} ${name}`;
1630
+ const title = busy ? task && task.status === "pending" ? busyTitle(t("queuedUninstallTitle")) : busyTitle(t("uninstalling")) : done ? t("uninstallResultTitle") : t("uninstallTitle");
1631
+ return (0, react.createElement)("div", {
1632
+ className: Modal_module_css_default.overlay,
1633
+ onClick: (e) => {
1634
+ if (e.target === e.currentTarget) onClose();
1635
+ }
1636
+ }, (0, react.createElement)("div", {
1637
+ className: Modal_module_css_default.modal,
1638
+ role: "dialog",
1639
+ "aria-modal": "true"
1640
+ }, (0, react.createElement)("div", { className: Modal_module_css_default.modalHead }, (0, react.createElement)("div", { className: busy ? `${Modal_module_css_default.modalTitle} ${task !== null && task.status === "pending" ? Modal_module_css_default.modalTitleQueued : Modal_module_css_default.modalTitleBusy}` : Modal_module_css_default.modalTitle }, title), (0, react.createElement)("button", {
1641
+ className: Modal_module_css_default.modalClose,
1642
+ "aria-label": t("confirmCancel"),
1643
+ onClick: () => onClose()
1644
+ }, (0, react.createElement)(CloseIcon))), done ? (0, react.createElement)(ResultView, {
1645
+ title: t("uninstallResultTitle"),
1646
+ desc: t("uninstallResultDesc"),
1647
+ t,
1648
+ restarting,
1649
+ onRestart,
1650
+ onClose
1651
+ }) : (0, react.createElement)("div", { className: Modal_module_css_default.modalBody }, (0, react.createElement)("div", { className: Modal_module_css_default.modalDesc }, t("uninstallDesc")), (0, react.createElement)("div", { className: Modal_module_css_default.modalRow }, (0, react.createElement)("span", { className: Modal_module_css_default.modalLabel }, t("confirmPlugin")), (0, react.createElement)("span", {
1652
+ className: Modal_module_css_default.modalValue,
1653
+ title: plugin.displayName ?? plugin.slug
1654
+ }, plugin.displayName ?? plugin.slug)), plugin.source?.repo ? (0, react.createElement)("div", { className: Modal_module_css_default.modalRow }, (0, react.createElement)("span", { className: Modal_module_css_default.modalLabel }, t("confirmSource")), (0, react.createElement)("a", {
1655
+ className: Modal_module_css_default.modalLink,
1656
+ href: pluginDetailUrl(plugin, langPath),
1657
+ target: "_blank",
1658
+ rel: "noopener noreferrer",
1659
+ title: plugin.source.repo
1660
+ }, (0, react.createElement)(LinkIcon), plugin.source.repo)) : null, task && task.status === "pending" ? (0, react.createElement)("div", { className: Modal_module_css_default.queuedHint }, t("queuedHint")) : null, task ? (0, react.createElement)(ProgressView, { task }) : null, (0, react.createElement)("div", { className: Modal_module_css_default.modalActions }, (0, react.createElement)("button", {
1661
+ className: Modal_module_css_default.modalCancel,
1662
+ onClick: onCancel
1663
+ }, t("confirmCancel")), (0, react.createElement)("button", {
1664
+ className: Modal_module_css_default.modalCopy,
1665
+ disabled: busy,
1666
+ onClick: onCopyCommand
1667
+ }, t("copyUninstallCommand")), (0, react.createElement)("button", {
1668
+ className: Modal_module_css_default.uninstallConfirm,
1669
+ disabled: busy,
1670
+ onClick: onConfirm
1671
+ }, busy ? task && task.status === "pending" ? t("queuedUninstallTitle") : t("uninstalling") : t("uninstall"))))));
1672
+ }
1673
+ /** 预填插件仓库的 GitHub Issue 链接:标题带插件名,正文附完整错误信息,方便用户一键反馈。 */
1674
+ /** 安装/卸载失败弹窗:布局与失败记录一致(类型徽标 + 仓库超链接 + 隐蔽复制按钮),报错完整展示,底部一键提交 Issue。 */
1675
+ function ErrorModal({ message, repo, kind, command, attempts, t, env, onCopy, onClose }) {
1676
+ const failureKind = classifyFailure(message);
1677
+ return (0, react.createElement)("div", {
1678
+ className: Modal_module_css_default.overlay,
1679
+ onClick: (e) => {
1680
+ if (e.target === e.currentTarget) onClose();
1681
+ }
1682
+ }, (0, react.createElement)("div", {
1683
+ className: Modal_module_css_default.errorModal,
1684
+ role: "dialog",
1685
+ "aria-modal": "true"
1686
+ }, (0, react.createElement)("div", { className: Modal_module_css_default.modalHead }, (0, react.createElement)("div", { className: Modal_module_css_default.errorTitle }, kind === "install" ? t("errorTitleInstall") : t("errorTitleUninstall")), (0, react.createElement)("button", {
1687
+ className: Modal_module_css_default.modalClose,
1688
+ "aria-label": t("errorClose"),
1689
+ onClick: onClose
1690
+ }, (0, react.createElement)(CloseIcon))), (0, react.createElement)("div", { className: Modal_module_css_default.modalBody }, (0, react.createElement)("div", { className: Modal_module_css_default.failRow }, (0, react.createElement)("div", { className: Modal_module_css_default.failHead }, (0, react.createElement)("span", { className: kind === "install" ? Modal_module_css_default.failKindInstall : Modal_module_css_default.failKindUninstall }, kind === "install" ? t("install") : t("uninstall")), repo ? (0, react.createElement)("a", {
1691
+ className: Modal_module_css_default.failRepo,
1692
+ href: pluginSiteUrl(repo),
1693
+ target: "_blank",
1694
+ rel: "noopener noreferrer",
1695
+ title: repo
1696
+ }, repo) : null, (0, react.createElement)("button", {
1697
+ className: Modal_module_css_default.errorCopySoft,
1698
+ onClick: () => onCopy(message)
1699
+ }, t("errorCopy"))), (0, react.createElement)("pre", { className: Modal_module_css_default.errorBox }, message), failureKind === "pluginPrepare" || failureKind === "pnpmIgnoredBuild" ? (0, react.createElement)("div", null, [(0, react.createElement)("div", { className: Modal_module_css_default.failPrepareHint }, failureKind === "pnpmIgnoredBuild" ? t("failIgnoredBuild") : /\[packaging\]/i.test(message) ? t("failPackagingHint") : t("failPrepareHint")), repo ? (0, react.createElement)("a", {
1700
+ className: Modal_module_css_default.failBigIssue,
1701
+ href: pluginIssueUrl(repo, message, env, command, attempts),
1702
+ target: "_blank",
1703
+ rel: "noopener noreferrer",
1704
+ title: t("failIssueHint")
1705
+ }, t("failIssueBig")) : null]) : repo ? (0, react.createElement)("a", {
1706
+ className: Modal_module_css_default.failBigIssue,
1707
+ href: pluginIssueUrl(repo, message, env, command, attempts),
1708
+ target: "_blank",
1709
+ rel: "noopener noreferrer",
1710
+ title: t("failIssueHint")
1711
+ }, t("failIssueBig")) : null), (0, react.createElement)("div", { className: Modal_module_css_default.modalActions }, (0, react.createElement)("button", {
1712
+ className: Modal_module_css_default.restartLater,
1713
+ onClick: onClose
1714
+ }, t("errorClose"))))));
1715
+ }
1716
+ /** 全局反馈 Toast:复制成功(反色)/ 安装完成(反色)/ 安装失败(红色)/ 卸载结果 */
1717
+ function Toast({ toast, t }) {
1718
+ const text = toast.kind === "copied" ? t("toastCopied") : toast.kind === "errCopied" ? t("errCopied") : toast.kind === "done" ? t("installDone") : toast.kind === "fail" ? t("installFail") : toast.kind === "removed" ? t("uninstallDone") : t("uninstallFail");
1719
+ const fail = toast.kind === "fail" || toast.kind === "removeFail";
1720
+ return (0, react.createElement)("div", {
1721
+ key: toast.id,
1722
+ className: fail ? `${Modal_module_css_default.toast} ${Modal_module_css_default.toastFail}` : Modal_module_css_default.toast
1723
+ }, text);
1724
+ }
1725
+ //#endregion
1726
+ //#region src/client/components/NotificationsModal.tsx
1727
+ /**
1728
+ * Notification-center dialog: a persistent log of every settled install /
1729
+ * remove task — successes and failures alike.
1730
+ *
1731
+ * Records are written to localStorage at settle time (see lib/failures.ts),
1732
+ * so a result is never lost even when the dialog was dismissed or the user
1733
+ * was away. Each entry carries a circular status badge (green check for
1734
+ * success, red cross for failure) with white glyph and message text;
1735
+ * failures keep their copy / fix / file-an-issue actions. Opened from the
1736
+ * header entry button.
1737
+ */
1738
+ /** 记录时间完整展示:YYYY-MM-DD HH:mm:ss(每条通知都带精确到秒的时间戳)。 */
1739
+ function fmtTime(at) {
1740
+ const d = new Date(at);
1741
+ const pad = (n) => String(n).padStart(2, "0");
1742
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
1743
+ }
1744
+ /** 圆形状态徽标内的白色图形:成功为对勾、失败为叉。 */
1745
+ function BadgeGlyph({ ok }) {
1746
+ return (0, react.createElement)("svg", {
1747
+ className: Modal_module_css_default.noticeBadgeIcon,
1748
+ viewBox: "0 0 16 16",
1749
+ width: 12,
1750
+ height: 12,
1751
+ fill: "none",
1752
+ "aria-hidden": "true"
1753
+ }, ok ? (0, react.createElement)("path", {
1754
+ d: "M3 8.5l3.5 3.5 6.5-6.5",
1755
+ stroke: "#ffffff",
1756
+ strokeWidth: 2,
1757
+ strokeLinecap: "round",
1758
+ strokeLinejoin: "round"
1759
+ }) : (0, react.createElement)("path", {
1760
+ d: "M4 4l8 8M12 4L4 12",
1761
+ stroke: "#ffffff",
1762
+ strokeWidth: 2,
1763
+ strokeLinecap: "round"
1764
+ }));
1765
+ }
1766
+ function NotificationsModal({ records, tasks, pendingRestarts, t, env, onClose, onCopy, onClear, onRemove, cancelTask, restarting, onRestart }) {
1767
+ return (0, react.createElement)("div", {
1768
+ className: Modal_module_css_default.overlay,
1769
+ onClick: (e) => {
1770
+ if (e.target === e.currentTarget) onClose();
1771
+ }
1772
+ }, (0, react.createElement)("div", {
1773
+ className: Modal_module_css_default.errorModal,
1774
+ role: "dialog",
1775
+ "aria-modal": "true"
1776
+ }, (0, react.createElement)("div", { className: Modal_module_css_default.modalHead }, (0, react.createElement)("div", { className: Modal_module_css_default.modalTitle }, t("notifications")), (0, react.createElement)("button", {
1777
+ className: Modal_module_css_default.modalClose,
1778
+ "aria-label": t("errorClose"),
1779
+ onClick: onClose
1780
+ }, (0, react.createElement)(CloseIcon))), (0, react.createElement)("div", { className: Modal_module_css_default.modalBody }, (0, react.createElement)("div", { className: Modal_module_css_default.errorHint }, t("notificationsDesc")), tasks.length > 0 || pendingRestarts.length > 0 ? (0, react.createElement)("div", null, [pendingRestarts.length > 0 ? (0, react.createElement)("div", { className: Modal_module_css_default.queueSection }, (0, react.createElement)("div", { className: Modal_module_css_default.queueSectionTitle }, t("sectionPendingRestart")), pendingRestarts.map((p) => (0, react.createElement)("div", {
1781
+ key: `restart-${p.target}`,
1782
+ className: Modal_module_css_default.queueRow
1783
+ }, (0, react.createElement)("div", { className: Modal_module_css_default.queueRowHead }, (0, react.createElement)("span", { className: `${Modal_module_css_default.failKind} ${p.kind === "uninstall" ? Modal_module_css_default.failKindUninstall : Modal_module_css_default.failKindInstall}` }, p.kind === "uninstall" ? t("uninstall") : t("install")), (0, react.createElement)("span", {
1784
+ className: Modal_module_css_default.queueRowTarget,
1785
+ title: p.target
1786
+ }, p.target)), p.desc ? (0, react.createElement)("div", {
1787
+ className: Modal_module_css_default.queueRowDesc,
1788
+ title: p.desc
1789
+ }, p.desc) : null, (0, react.createElement)("div", { className: Modal_module_css_default.queueRowBody }, (0, react.createElement)("span", { className: Modal_module_css_default.pendingRowStatus }, p.kind === "uninstall" ? t("restartPendingHintUninstall") : t("restartPendingHint")), (0, react.createElement)("span", { className: Modal_module_css_default.pendingRowActions }, (0, react.createElement)("button", {
1790
+ className: Modal_module_css_default.restartLater,
1791
+ disabled: restarting,
1792
+ onClick: onClose
1793
+ }, t("restartLater")), (0, react.createElement)("button", {
1794
+ className: Modal_module_css_default.restartNow,
1795
+ disabled: restarting,
1796
+ onClick: onRestart
1797
+ }, restarting ? t("restarting") : t("restartNow"))))))) : null, tasks.length > 0 ? (0, react.createElement)("div", { className: Modal_module_css_default.queueSection }, (0, react.createElement)("div", { className: Modal_module_css_default.queueSectionTitle }, t("sectionInProgress")), tasks.map((q) => (0, react.createElement)("div", {
1798
+ key: q.id,
1799
+ className: Modal_module_css_default.queueRow
1800
+ }, (0, react.createElement)("div", { className: Modal_module_css_default.queueRowHead }, (0, react.createElement)("span", { className: `${Modal_module_css_default.failKind} ${q.kind === "install" ? Modal_module_css_default.failKindInstall : Modal_module_css_default.failKindUninstall}` }, q.kind === "install" ? t("install") : t("uninstall")), (0, react.createElement)("span", {
1801
+ className: Modal_module_css_default.queueRowTarget,
1802
+ title: q.target
1803
+ }, q.target), (0, react.createElement)("button", {
1804
+ className: Modal_module_css_default.stripCancel,
1805
+ disabled: q.status === "cancelling",
1806
+ onClick: (e) => {
1807
+ e.stopPropagation();
1808
+ cancelTask(q.id);
1809
+ }
1810
+ }, t("cancelTask"))), q.desc ? (0, react.createElement)("div", {
1811
+ className: Modal_module_css_default.queueRowDesc,
1812
+ title: q.desc
1813
+ }, q.desc) : null, (0, react.createElement)("div", { className: Modal_module_css_default.queueRowBody }, (0, react.createElement)("span", { className: Modal_module_css_default.queueRowStatus }, q.status === "running" ? q.kind === "install" ? t("installing") : t("uninstalling") : q.status === "cancelling" ? t("cancelling") : q.kind === "install" ? t("queuedTitle") : t("queuedUninstallTitle")), (0, react.createElement)("div", { className: `${Modal_module_css_default.progressTrack} ${Modal_module_css_default.queueRowTrack}` }, (0, react.createElement)("div", {
1814
+ className: Modal_module_css_default.progressFill,
1815
+ style: { width: `${q.progress}%` }
1816
+ })), (0, react.createElement)("span", { className: Modal_module_css_default.queueRowPct }, `${q.progress}%`))))) : null]) : null, records.length === 0 ? (0, react.createElement)("div", { className: Modal_module_css_default.failEmpty }, t("notificationsEmpty")) : (0, react.createElement)("div", { className: Modal_module_css_default.noticeList }, records.map((r) => {
1817
+ return (0, react.createElement)("div", {
1818
+ key: r.id,
1819
+ className: Modal_module_css_default.noticeRow
1820
+ }, (0, react.createElement)("div", { className: Modal_module_css_default.noticeRowMain }, (0, react.createElement)("div", { className: r.ok ? Modal_module_css_default.noticeBadgeOk : Modal_module_css_default.noticeBadgeFail }, (0, react.createElement)(BadgeGlyph, { ok: r.ok })), (0, react.createElement)("div", { className: Modal_module_css_default.noticeMain }, (0, react.createElement)("div", { className: Modal_module_css_default.noticeHead }, (0, react.createElement)("span", { className: r.ok ? Modal_module_css_default.noticeTextOk : Modal_module_css_default.noticeTextFail }, r.ok ? r.kind === "install" ? t("installDone") : t("uninstallDone") : r.kind === "install" ? t("errorTitleInstall") : t("errorTitleUninstall")), r.repo ? (0, react.createElement)("a", {
1821
+ className: Modal_module_css_default.failRepo,
1822
+ href: pluginSiteUrl(r.repo),
1823
+ target: "_blank",
1824
+ rel: "noopener noreferrer",
1825
+ title: r.repo
1826
+ }, r.repo) : null, !r.ok && (0, react.createElement)("button", {
1827
+ className: Modal_module_css_default.failCopy,
1828
+ onClick: () => onCopy(r.message)
1829
+ }, t("failCopy")), (0, react.createElement)("button", {
1830
+ className: Modal_module_css_default.noticeRemove,
1831
+ "aria-label": t("removeNotification"),
1832
+ title: t("removeNotification"),
1833
+ onClick: (e) => {
1834
+ e.stopPropagation();
1835
+ onRemove(r.id);
1836
+ }
1837
+ }, (0, react.createElement)(CloseIcon))), !r.ok && (() => {
1838
+ const kind = classifyFailure(r.message);
1839
+ if (kind === "pluginPrepare" || kind === "pnpmIgnoredBuild") return (0, react.createElement)("div", null, [(0, react.createElement)("div", { className: Modal_module_css_default.failPrepareHint }, kind === "pnpmIgnoredBuild" ? t("failIgnoredBuild") : /\[packaging\]/i.test(r.message) ? t("failPackagingHint") : t("failPrepareHint")), r.repo ? (0, react.createElement)("a", {
1840
+ className: Modal_module_css_default.failBigIssue,
1841
+ href: pluginIssueUrl(r.repo, r.message, env, r.command, r.attempts),
1842
+ target: "_blank",
1843
+ rel: "noopener noreferrer",
1844
+ title: t("failIssueHint")
1845
+ }, t("failIssueBig")) : null]);
1846
+ return r.repo ? (0, react.createElement)("a", {
1847
+ className: Modal_module_css_default.failBigIssue,
1848
+ href: pluginIssueUrl(r.repo, r.message, env, r.command, r.attempts),
1849
+ target: "_blank",
1850
+ rel: "noopener noreferrer",
1851
+ title: t("failIssueHint")
1852
+ }, t("failIssueBig")) : null;
1853
+ })())), (0, react.createElement)("div", { className: Modal_module_css_default.noticeFoot }, (0, react.createElement)("span", {
1854
+ className: Modal_module_css_default.noticeTime,
1855
+ title: new Date(r.at).toLocaleString()
1856
+ }, fmtTime(r.at))));
1857
+ })), (0, react.createElement)("div", { className: Modal_module_css_default.modalActions }, records.length > 0 ? (0, react.createElement)("button", {
1858
+ className: Modal_module_css_default.failClear,
1859
+ onClick: onClear
1860
+ }, t("notificationsClear")) : null, (0, react.createElement)("button", {
1861
+ className: Modal_module_css_default.restartNow,
1862
+ onClick: onClose
1863
+ }, t("errorClose"))))));
1864
+ }
1865
+ //#endregion
1866
+ //#region src/client/components/CatalogHeader.tsx
1867
+ /**
1868
+ * Section header: brand title row (H1 + open-site button) + tagline, followed
1869
+ * by the purple ad banner that promotes the catalog stats.
1870
+ */
1871
+ function CatalogHeader({ t, langPath, statsTotal, statsVerified, onToggleLang }) {
1872
+ return (0, react.createElement)(react.Fragment, null, (0, react.createElement)("div", { className: Header_module_css_default.header }, (0, react.createElement)("div", { className: Header_module_css_default.headerTitleRow }, (0, react.createElement)("a", {
1873
+ className: Header_module_css_default.brandTitle,
1874
+ href: `${SITE_URL}${langPath}`,
1875
+ target: "_blank",
1876
+ rel: "noopener noreferrer",
1877
+ title: t("openHint"),
1878
+ "aria-label": t("openHint")
1879
+ }, (0, react.createElement)(LogoIcon), (0, react.createElement)("h1", { className: Header_module_css_default.title }, t("title"), (0, react.createElement)("span", { className: Header_module_css_default.version }, ` v${PLUGIN_VERSION}`))), (0, react.createElement)("div", { className: Header_module_css_default.headerRight }, (0, react.createElement)("button", {
1880
+ className: Header_module_css_default.langBtn,
1881
+ type: "button",
1882
+ onClick: onToggleLang,
1883
+ title: t("toggleLangHint"),
1884
+ "aria-label": t("toggleLangHint")
1885
+ }, langPath === "zh/" ? "EN" : "中文"), (0, react.createElement)("a", {
1886
+ className: Header_module_css_default.githubLink,
1887
+ href: GITHUB_URL,
1888
+ target: "_blank",
1889
+ rel: "noopener noreferrer",
1890
+ title: t("githubHint"),
1891
+ "aria-label": t("githubHint")
1892
+ }, (0, react.createElement)(GitHubIcon)))), (0, react.createElement)("a", {
1893
+ className: Header_module_css_default.taglineLink,
1894
+ href: `${SITE_URL}${langPath}`,
1895
+ target: "_blank",
1896
+ rel: "noopener noreferrer",
1897
+ title: t("openHint")
1898
+ }, (0, react.createElement)("div", { className: Header_module_css_default.tagline }, t("tagline", {
1899
+ total: statsTotal,
1900
+ verified: statsVerified
1901
+ })))), (0, react.createElement)("a", {
1902
+ className: Header_module_css_default.adBanner,
1903
+ href: `${SITE_URL}${langPath}`,
1904
+ target: "_blank",
1905
+ rel: "noopener noreferrer"
1906
+ }, (0, react.createElement)("span", { className: Header_module_css_default.adBadge }, t("adBadge")), (0, react.createElement)("span", { className: Header_module_css_default.adText }, t("ad", {
1907
+ total: statsTotal,
1908
+ verified: statsVerified
1909
+ })), (0, react.createElement)("span", { className: Header_module_css_default.adArrow }, "↗")));
1910
+ }
1911
+ //#endregion
1912
+ //#region src/client/components/CategoryTabs.tsx
1913
+ /**
1914
+ * Category tabs: an "all" chip followed by one chip per catalog category.
1915
+ * The "all" chip carries the total plugin count; the per-category chips
1916
+ * carry no counts so their widths stay uniform.
1917
+ */
1918
+ function CategoryTabs({ category, setCategory, allLabel, totalCount, langKey }) {
1919
+ return (0, react.createElement)("div", { className: Header_module_css_default.tabs }, (0, react.createElement)("button", {
1920
+ key: "all",
1921
+ className: category === "all" ? Header_module_css_default.tabActive : Header_module_css_default.tab,
1922
+ onClick: () => setCategory("all")
1923
+ }, allLabel, (0, react.createElement)("span", { className: Header_module_css_default.tabCount }, totalCount)), CATEGORY_ORDER.map((id) => (0, react.createElement)("button", {
1924
+ key: id,
1925
+ className: category === id ? Header_module_css_default.tabActive : Header_module_css_default.tab,
1926
+ onClick: () => setCategory(id)
1927
+ }, categoryLabel(CATEGORY_SHORT_LABELS, id, langKey))));
1928
+ }
1929
+ //#endregion
1930
+ //#region \0dsh-css:src/client/styles/Dropdown.module.css.mjs
1931
+ const css$2 = ".B_Gxsq_dropdown{flex-shrink:0;display:inline-flex;position:relative}.B_Gxsq_dropdownBtn{height:24px;color:var(--hub-text-primary);cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap;background:0 0;border:none;border-radius:6px;align-items:center;gap:6px;padding:0 8px;font-size:12px;line-height:22px;transition:background .12s;display:inline-flex}.B_Gxsq_dropdownBtn:hover{background:var(--hub-bg-hover)}.B_Gxsq_dropdownLabel{text-overflow:ellipsis;min-width:0;overflow:hidden}.B_Gxsq_dropdownArrow,.B_Gxsq_dropdownArrowOpen{color:var(--hub-text-tertiary);justify-content:center;align-items:center;transition:transform .12s;display:inline-flex}.B_Gxsq_dropdownArrowOpen{transform:rotate(180deg)}.B_Gxsq_dropdownPanel{z-index:50;background:var(--hub-bg-1);border:1px solid var(--hub-border-2);border-radius:8px;flex-direction:column;gap:2px;min-width:100%;padding:4px;display:flex;position:absolute;top:calc(100% + 4px);left:0;box-shadow:0 8px 24px #00000024}.B_Gxsq_dropdownItem,.B_Gxsq_dropdownItemActive{cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap;text-align:left;border:none;border-radius:6px;justify-content:space-between;align-items:center;gap:10px;padding:4px 9px;font-size:12px;line-height:18px;transition:background .12s,color .12s;display:flex}.B_Gxsq_dropdownItem{color:var(--hub-text-primary);background:0 0}.B_Gxsq_dropdownItem:hover{background:var(--hub-bg-hover)}.B_Gxsq_dropdownItemActive{color:var(--hub-text-on-fill);background:var(--hub-btn-fill)}.B_Gxsq_dropdownItemLabel{text-overflow:ellipsis;min-width:0;overflow:hidden}.B_Gxsq_dropdownCount,.B_Gxsq_dropdownCountActive{text-align:center;border-radius:999px;min-width:16px;padding:0 6px;font-size:10px;line-height:14px}.B_Gxsq_dropdownCount{color:var(--hub-text-tertiary);background:var(--hub-bg-btn)}.B_Gxsq_dropdownItemActive .B_Gxsq_dropdownCountActive{color:var(--hub-text-on-fill);background:var(--hub-bg-on-fill)}";
1932
+ const tagId$2 = "dsh-plugin/Dropdown.module.css";
1933
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
1934
+ const tag = document.createElement("style");
1935
+ tag.dataset.plugin = "dsh-plugin";
1936
+ tag.dataset.pluginCss = tagId$2;
1937
+ tag.textContent = css$2;
1938
+ document.head.appendChild(tag);
1939
+ }
1940
+ var Dropdown_module_css_default = {
1941
+ "dropdownPanel": "B_Gxsq_dropdownPanel",
1942
+ "dropdownArrowOpen": "B_Gxsq_dropdownArrowOpen",
1943
+ "dropdownItemActive": "B_Gxsq_dropdownItemActive",
1944
+ "dropdownCountActive": "B_Gxsq_dropdownCountActive",
1945
+ "dropdownLabel": "B_Gxsq_dropdownLabel",
1946
+ "dropdown": "B_Gxsq_dropdown",
1947
+ "dropdownArrow": "B_Gxsq_dropdownArrow",
1948
+ "dropdownBtn": "B_Gxsq_dropdownBtn",
1949
+ "dropdownItem": "B_Gxsq_dropdownItem",
1950
+ "dropdownItemLabel": "B_Gxsq_dropdownItemLabel",
1951
+ "dropdownCount": "B_Gxsq_dropdownCount"
1952
+ };
1953
+ //#endregion
1954
+ //#region src/client/components/Dropdown.tsx
1955
+ /**
1956
+ * Custom dropdown (native <select> replacement).
1957
+ *
1958
+ * macOS/浏览器原生下拉无法跟随宿主主题,这里自绘:触发器按钮 + 绝对定位面板,
1959
+ * 点外部/Esc 关闭;选项 hover 加重底色、激活项深色实心高亮,全部走 --dsw-alias-* 主题变量。
1960
+ */
1961
+ function Dropdown({ value, options, onChange, title }) {
1962
+ const [open, setOpen] = (0, react.useState)(false);
1963
+ const rootRef = (0, react.useRef)(null);
1964
+ (0, react.useEffect)(() => {
1965
+ if (!open) return;
1966
+ const onDoc = (e) => {
1967
+ if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
1968
+ };
1969
+ const onKey = (e) => {
1970
+ if (e.key === "Escape") setOpen(false);
1971
+ };
1972
+ document.addEventListener("mousedown", onDoc);
1973
+ document.addEventListener("keydown", onKey);
1974
+ return () => {
1975
+ document.removeEventListener("mousedown", onDoc);
1976
+ document.removeEventListener("keydown", onKey);
1977
+ };
1978
+ }, [open]);
1979
+ const current = options.find((o) => o.value === value);
1980
+ return (0, react.createElement)("div", {
1981
+ className: Dropdown_module_css_default.dropdown,
1982
+ ref: rootRef
1983
+ }, (0, react.createElement)("button", {
1984
+ className: Dropdown_module_css_default.dropdownBtn,
1985
+ type: "button",
1986
+ title,
1987
+ "aria-haspopup": "listbox",
1988
+ "aria-expanded": open,
1989
+ onClick: () => setOpen((v) => !v)
1990
+ }, (0, react.createElement)("span", { className: Dropdown_module_css_default.dropdownLabel }, current?.label ?? value), (0, react.createElement)("span", { className: open ? Dropdown_module_css_default.dropdownArrowOpen : Dropdown_module_css_default.dropdownArrow }, (0, react.createElement)(ChevronDownIcon))), open && (0, react.createElement)("div", {
1991
+ className: Dropdown_module_css_default.dropdownPanel,
1992
+ role: "listbox"
1993
+ }, options.map((o) => (0, react.createElement)("button", {
1994
+ key: o.value,
1995
+ type: "button",
1996
+ role: "option",
1997
+ "aria-selected": o.value === value,
1998
+ className: o.value === value ? Dropdown_module_css_default.dropdownItemActive : Dropdown_module_css_default.dropdownItem,
1999
+ onClick: () => {
2000
+ onChange(o.value);
2001
+ setOpen(false);
2002
+ }
2003
+ }, (0, react.createElement)("span", { className: Dropdown_module_css_default.dropdownItemLabel }, o.label), typeof o.count === "number" ? (0, react.createElement)("span", { className: o.value === value ? Dropdown_module_css_default.dropdownCountActive : Dropdown_module_css_default.dropdownCount }, o.count) : null))));
2004
+ }
2005
+ //#endregion
2006
+ //#region src/client/components/CatalogControls.tsx
2007
+ /**
2008
+ * Toolbar above the list: search input + sort dropdown + installed / not
2009
+ * installed filter buttons + the notifications entry. All buttons share the
2010
+ * same size (24px high, like the notification entry button).
2011
+ */
2012
+ function CatalogControls({ query, setQuery, sort, setSort, installedFilter, setInstalledFilter, installedCount, notInstalledCount, t, resultText, noticeCount, onOpenNotifications }) {
2013
+ return (0, react.createElement)(react.Fragment, null, (0, react.createElement)("div", { className: Header_module_css_default.searchRow }, (0, react.createElement)("input", {
2014
+ className: Header_module_css_default.search,
2015
+ type: "search",
2016
+ placeholder: t("search"),
2017
+ value: query,
2018
+ spellCheck: false,
2019
+ onInput: (e) => setQuery(e.target.value)
2020
+ })), (0, react.createElement)("div", { className: Header_module_css_default.controls }, resultText ? (0, react.createElement)("span", { className: Header_module_css_default.filterResults }, ...resultText.split(/(\d+)/).map((part, i) => /^\d+$/.test(part) ? (0, react.createElement)("span", {
2021
+ key: i,
2022
+ className: Header_module_css_default.resultCount
2023
+ }, part) : part)) : null, (0, react.createElement)("button", {
2024
+ className: installedFilter === "all" ? Header_module_css_default.installedBtnActive : Header_module_css_default.installedBtn,
2025
+ onClick: () => setInstalledFilter("all"),
2026
+ title: t("filterAllHint"),
2027
+ "aria-pressed": installedFilter === "all"
2028
+ }, t("all")), Dropdown({
2029
+ value: sort,
2030
+ options: SORTS.map((key) => ({
2031
+ value: key,
2032
+ label: t(key)
2033
+ })),
2034
+ onChange: setSort
2035
+ }), (0, react.createElement)("button", {
2036
+ className: installedFilter === "installed" ? Header_module_css_default.installedBtnActive : installedCount === 0 ? Header_module_css_default.installedBtnDisabled : Header_module_css_default.installedBtn,
2037
+ onClick: () => setInstalledFilter("installed"),
2038
+ disabled: installedCount === 0,
2039
+ title: installedCount === 0 ? t("filterInstalledNone") : t("filterInstalledHint"),
2040
+ "aria-pressed": installedFilter === "installed"
2041
+ }, t("installed"), (0, react.createElement)("span", { className: installedFilter === "installed" ? Header_module_css_default.segCountActive : Header_module_css_default.segCount }, installedCount)), (0, react.createElement)("button", {
2042
+ className: installedFilter === "notInstalled" ? Header_module_css_default.installedBtnActive : notInstalledCount === 0 ? Header_module_css_default.installedBtnDisabled : Header_module_css_default.installedBtn,
2043
+ onClick: () => setInstalledFilter("notInstalled"),
2044
+ disabled: notInstalledCount === 0,
2045
+ title: notInstalledCount === 0 ? t("filterNotInstalledNone") : t("filterNotInstalledHint"),
2046
+ "aria-pressed": installedFilter === "notInstalled"
2047
+ }, t("notInstalled"), (0, react.createElement)("span", { className: installedFilter === "notInstalled" ? Header_module_css_default.segCountActive : Header_module_css_default.segCount }, notInstalledCount)), (0, react.createElement)("button", {
2048
+ className: Header_module_css_default.failBtn,
2049
+ onClick: onOpenNotifications,
2050
+ title: t("notificationsHint"),
2051
+ "aria-label": t("notificationsHint")
2052
+ }, t("notificationsBtn"), noticeCount > 0 ? (0, react.createElement)("span", { className: Header_module_css_default.failBadge }, noticeCount) : null)));
2053
+ }
2054
+ //#endregion
2055
+ //#region \0dsh-css:src/client/styles/List.module.css.mjs
2056
+ const css$1 = "._3XaZHa_body{flex-direction:column;flex:1;min-height:0;display:flex}._3XaZHa_list{flex-direction:column;flex:1;gap:6px;min-height:0;padding:2px 4px 4px 2px;display:flex;overflow-y:auto}._3XaZHa_card{border:1px solid var(--hub-border-2);background:var(--hub-bg-1);border-radius:8px;justify-content:space-between;align-items:stretch;gap:12px;padding:9px 12px;transition:border-color .12s,background .12s;display:flex}._3XaZHa_card:hover{border-color:var(--hub-brand);background:var(--hub-bg-2)}._3XaZHa_cardMain{flex-direction:column;gap:4px;min-width:0;display:flex}._3XaZHa_cardHead{align-items:center;gap:6px;min-width:0;display:flex}._3XaZHa_cardTitle{white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:600;line-height:18px;overflow:hidden}._3XaZHa_categoryBadge,._3XaZHa_verified,._3XaZHa_versionBadge,._3XaZHa_updateBadge{border:1px solid #0000;border-radius:4px;flex-shrink:0;padding:0 6px;font-size:10px;line-height:16px}._3XaZHa_categoryBadge{color:var(--hub-brand);border-color:var(--hub-border-ghost)}._3XaZHa_verified{color:var(--hub-success);border-color:var(--hub-success-border)}._3XaZHa_versionBadge{color:var(--hub-text-secondary);border-color:var(--hub-border-input)}._3XaZHa_updateBadge{color:var(--hub-warn);border-color:var(--hub-warn-border);background:var(--hub-warn-tint)}._3XaZHa_desc{color:var(--hub-text-secondary);-webkit-line-clamp:2;-webkit-box-orient:vertical;margin:0;font-size:12px;line-height:18px;display:-webkit-box;overflow:hidden}._3XaZHa_topics{flex-wrap:wrap;align-items:center;gap:4px;min-width:0;display:flex}._3XaZHa_topic{color:var(--hub-text-tertiary);background:var(--hub-bg-3);white-space:nowrap;border-radius:4px;flex-shrink:0;padding:0 6px;font-size:10px;line-height:16px}._3XaZHa_cardSide{flex-direction:column;flex-shrink:0;justify-content:space-between;align-items:flex-end;gap:6px;display:flex}._3XaZHa_stats{flex-direction:column;align-items:flex-end;gap:1px;display:flex}._3XaZHa_star{color:var(--hub-warn);white-space:nowrap;font-size:11px;line-height:16px}._3XaZHa_fork{color:var(--hub-text-tertiary);white-space:nowrap;font-size:11px;line-height:16px}._3XaZHa_date{color:var(--hub-text-tertiary);white-space:nowrap;font-size:10px;line-height:14px}._3XaZHa_installBtn,._3XaZHa_installBtnCopied,._3XaZHa_installBtnInstalled,._3XaZHa_installBtnUpdate,._3XaZHa_uninstallBtn,._3XaZHa_detailBtn{cursor:pointer;border-radius:6px;padding:2px 10px;font-size:11px;line-height:18px;transition:color .12s,border-color .12s,background .12s}._3XaZHa_installBtnInstalled{color:var(--hub-success);background:var(--hub-success-tint);cursor:default;-webkit-user-select:none;user-select:none;border:none}._3XaZHa_installBtnUpdate{color:#fff;background:var(--hub-warn);-webkit-user-select:none;user-select:none;border:none;font-weight:500}._3XaZHa_installBtnUpdate:hover{background:var(--hub-warn-strong)}._3XaZHa_uninstallBtn{color:var(--hub-danger);background:var(--hub-danger-tint);-webkit-user-select:none;user-select:none;border:none}._3XaZHa_uninstallBtn:hover{color:#fff;background:var(--hub-danger-hover)}._3XaZHa_installBtn{color:var(--hub-text-on-fill);background:var(--hub-btn-fill);-webkit-user-select:none;user-select:none;border:none;font-weight:500}._3XaZHa_installBtn:hover{background:var(--hub-btn-hover)}._3XaZHa_detailBtn{color:var(--hub-text-secondary);-webkit-user-select:none;user-select:none;background:0 0;border:none;align-items:center;text-decoration:none;display:inline-flex}._3XaZHa_detailBtn:hover{color:var(--hub-text-primary);background:var(--hub-bg-hover)}._3XaZHa_actions{align-items:center;gap:6px;display:flex}._3XaZHa_installBtnCopied{color:var(--hub-success);background:var(--hub-success-tint);-webkit-user-select:none;user-select:none;border:none}._3XaZHa_state{text-align:center;min-height:160px;color:var(--hub-text-tertiary);flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;padding:24px;font-size:12px;line-height:18px;display:flex}._3XaZHa_stateTitle{color:var(--hub-text-primary);font-size:13px;font-weight:600}._3XaZHa_stateDesc{max-width:420px}._3XaZHa_retryBtn{color:var(--hub-text-secondary);cursor:pointer;-webkit-user-select:none;user-select:none;background:0 0;border:none;border-radius:6px;margin-top:4px;padding:4px 12px;font-size:12px;line-height:18px}._3XaZHa_retryBtn:hover{color:var(--hub-text-primary);background:var(--hub-bg-hover)}._3XaZHa_footer{border-top:1px solid var(--hub-border-1);flex-shrink:0;justify-content:flex-end;align-items:center;gap:8px;padding:6px 4px 0;display:flex}._3XaZHa_footLink{color:var(--hub-brand);white-space:nowrap;-webkit-user-select:none;user-select:none;font-size:11px;line-height:16px;text-decoration:none}._3XaZHa_footLink:hover{text-decoration:underline}";
2057
+ const tagId$1 = "dsh-plugin/List.module.css";
2058
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
2059
+ const tag = document.createElement("style");
2060
+ tag.dataset.plugin = "dsh-plugin";
2061
+ tag.dataset.pluginCss = tagId$1;
2062
+ tag.textContent = css$1;
2063
+ document.head.appendChild(tag);
2064
+ }
2065
+ var List_module_css_default = {
2066
+ "topic": "_3XaZHa_topic",
2067
+ "verified": "_3XaZHa_verified",
2068
+ "uninstallBtn": "_3XaZHa_uninstallBtn",
2069
+ "actions": "_3XaZHa_actions",
2070
+ "stateTitle": "_3XaZHa_stateTitle",
2071
+ "cardMain": "_3XaZHa_cardMain",
2072
+ "installBtn": "_3XaZHa_installBtn",
2073
+ "cardTitle": "_3XaZHa_cardTitle",
2074
+ "star": "_3XaZHa_star",
2075
+ "card": "_3XaZHa_card",
2076
+ "categoryBadge": "_3XaZHa_categoryBadge",
2077
+ "fork": "_3XaZHa_fork",
2078
+ "stateDesc": "_3XaZHa_stateDesc",
2079
+ "retryBtn": "_3XaZHa_retryBtn",
2080
+ "updateBadge": "_3XaZHa_updateBadge",
2081
+ "detailBtn": "_3XaZHa_detailBtn",
2082
+ "versionBadge": "_3XaZHa_versionBadge",
2083
+ "state": "_3XaZHa_state",
2084
+ "date": "_3XaZHa_date",
2085
+ "stats": "_3XaZHa_stats",
2086
+ "body": "_3XaZHa_body",
2087
+ "cardHead": "_3XaZHa_cardHead",
2088
+ "list": "_3XaZHa_list",
2089
+ "installBtnCopied": "_3XaZHa_installBtnCopied",
2090
+ "cardSide": "_3XaZHa_cardSide",
2091
+ "footLink": "_3XaZHa_footLink",
2092
+ "topics": "_3XaZHa_topics",
2093
+ "installBtnInstalled": "_3XaZHa_installBtnInstalled",
2094
+ "desc": "_3XaZHa_desc",
2095
+ "installBtnUpdate": "_3XaZHa_installBtnUpdate",
2096
+ "footer": "_3XaZHa_footer"
2097
+ };
2098
+ //#endregion
2099
+ //#region src/client/lib/format.ts
2100
+ /** 星数/分支数的紧凑展示:<1k 原样;千级 1.2k;十万级取整。 */
2101
+ function fmtStars(count) {
2102
+ if (!count || count <= 0) return "0";
2103
+ if (count < 1e3) return String(count);
2104
+ const k = count / 1e3;
2105
+ return `${k >= 100 ? Math.round(k) : k.toFixed(1).replace(/\.0$/, "")}k`;
2106
+ }
2107
+ /** 相对时间:今天 / N 天前 / N 个月前 / N 年前。 */
2108
+ function relTime(iso, t) {
2109
+ if (!iso) return "";
2110
+ const days = Math.floor((Date.now() - new Date(iso).getTime()) / 864e5);
2111
+ if (days <= 0) return t("today");
2112
+ if (days < 30) return t("daysAgo", { days });
2113
+ if (days < 365) return t("monthsAgo", { months: Math.floor(days / 30) });
2114
+ return t("yearsAgo", { years: Math.floor(days / 365) });
2115
+ }
2116
+ //#endregion
2117
+ //#region src/client/components/PluginCard.tsx
2118
+ /**
2119
+ * One plugin card in the catalog list: name/category/verified badge,
2120
+ * description + topics, star/fork/date stats, and the detail/install/
2121
+ * uninstall actions.
2122
+ */
2123
+ function PluginCard({ plugin: p, copied, installedName, installedVersion, hasUpdate, t, langKey, langPath, onInstall, onUninstall }) {
2124
+ const repo = p.source?.repo ?? "";
2125
+ const isCopied = copied === repo;
2126
+ const isInstalled = installedName(p) !== null;
2127
+ const update = hasUpdate(p);
2128
+ const versionShown = isInstalled ? installedVersion(p) : p.version;
2129
+ return (0, react.createElement)("div", { className: List_module_css_default.card }, (0, react.createElement)("div", { className: List_module_css_default.cardMain }, (0, react.createElement)("div", { className: List_module_css_default.cardHead }, (0, react.createElement)("div", {
2130
+ className: List_module_css_default.cardTitle,
2131
+ title: p.description ?? ""
2132
+ }, p.displayName ?? p.slug), versionShown ? (0, react.createElement)("span", {
2133
+ className: List_module_css_default.versionBadge,
2134
+ title: t("version")
2135
+ }, versionShown) : null, update ? (0, react.createElement)("span", {
2136
+ className: List_module_css_default.updateBadge,
2137
+ title: t("updateAvailableHint")
2138
+ }, t("updateAvailable")) : null, p.category ? (0, react.createElement)("span", { className: List_module_css_default.categoryBadge }, categoryLabel(CATEGORY_LABELS, p.category, langKey)) : null, p.compatibility?.status === "verified" ? (0, react.createElement)("span", { className: List_module_css_default.verified }, t("verified")) : null), p.description ? (0, react.createElement)("p", { className: List_module_css_default.desc }, p.description) : null, (p.topics?.length ?? 0) > 0 ? (0, react.createElement)("div", { className: List_module_css_default.topics }, p.topics.slice(0, 3).map((topic) => (0, react.createElement)("span", {
2139
+ key: topic,
2140
+ className: List_module_css_default.topic
2141
+ }, topic))) : null), (0, react.createElement)("div", { className: List_module_css_default.cardSide }, (0, react.createElement)("div", { className: List_module_css_default.stats }, (0, react.createElement)("span", { className: List_module_css_default.star }, "★ ", fmtStars(p.stats?.stargazers_count)), (0, react.createElement)("span", { className: List_module_css_default.fork }, t("fork"), " ", fmtStars(p.stats?.forks_count)), (0, react.createElement)("span", { className: List_module_css_default.date }, relTime(p.dates?.repoUpdatedAt, t))), repo ? (0, react.createElement)("div", { className: List_module_css_default.actions }, (0, react.createElement)("a", {
2142
+ className: List_module_css_default.detailBtn,
2143
+ href: pluginDetailUrl(p, langPath),
2144
+ target: "_blank",
2145
+ rel: "noopener noreferrer",
2146
+ title: p.slug
2147
+ }, t("detail")), isInstalled ? update ? (0, react.createElement)("button", {
2148
+ className: List_module_css_default.installBtnUpdate,
2149
+ title: t("updateAvailableHint"),
2150
+ onClick: () => onInstall(p, { update: true })
2151
+ }, t("update")) : (0, react.createElement)("button", {
2152
+ className: List_module_css_default.installBtnInstalled,
2153
+ disabled: true,
2154
+ title: t("installed")
2155
+ }, t("installed")) : (0, react.createElement)("button", {
2156
+ className: isCopied ? List_module_css_default.installBtnCopied : List_module_css_default.installBtn,
2157
+ onClick: () => onInstall(p)
2158
+ }, t("install")), isInstalled ? (0, react.createElement)("button", {
2159
+ className: List_module_css_default.uninstallBtn,
2160
+ onClick: () => onUninstall(p)
2161
+ }, t("uninstall")) : null) : null));
2162
+ }
2163
+ //#endregion
2164
+ //#region src/client/components/CatalogList.tsx
2165
+ /**
2166
+ * Catalog list: the scrollable container with the loading / failed / empty
2167
+ * states, the plugin cards, and the count footer. Resets scroll position when
2168
+ * the category or install-status filter changes so the replaced content is
2169
+ * not mistaken for a no-op update.
2170
+ */
2171
+ function CatalogList({ plugins, failed, visible, total, t, langPath, reload, category, installedFilter, copied, installedName, installedVersion, hasUpdate, langKey, onInstall, onUninstall }) {
2172
+ /** 列表滚动容器:分类/搜索切换后列表内容替换但 scrollTop 保留,会让用户误以为列表没更新,需重置回顶部 */
2173
+ const listRef = (0, react.useRef)(null);
2174
+ (0, react.useEffect)(() => {
2175
+ listRef.current?.scrollTo({ top: 0 });
2176
+ }, [category, installedFilter]);
2177
+ return (0, react.createElement)("div", { className: List_module_css_default.body }, (0, react.createElement)("div", {
2178
+ ref: listRef,
2179
+ className: List_module_css_default.list
2180
+ }, plugins === null && !failed && (0, react.createElement)("div", { className: List_module_css_default.state }, t("loading")), failed && (0, react.createElement)("div", { className: List_module_css_default.state }, (0, react.createElement)("div", { className: List_module_css_default.stateTitle }, t("failed")), (0, react.createElement)("div", { className: List_module_css_default.stateDesc }, t("failedDesc")), (0, react.createElement)("button", {
2181
+ className: List_module_css_default.retryBtn,
2182
+ onClick: () => reload()
2183
+ }, t("retry"))), plugins !== null && !failed && visible.length === 0 && (0, react.createElement)("div", { className: List_module_css_default.state }, (0, react.createElement)("div", { className: List_module_css_default.stateTitle }, t("noResult")), (0, react.createElement)("div", { className: List_module_css_default.stateDesc }, t("noResultDesc"))), plugins !== null && !failed && visible.map((p) => (0, react.createElement)(PluginCard, {
2184
+ key: p.ownerSlug ? `${p.ownerSlug}/${p.slug}` : p.slug,
2185
+ plugin: p,
2186
+ copied,
2187
+ installedName,
2188
+ installedVersion,
2189
+ hasUpdate,
2190
+ t,
2191
+ langKey,
2192
+ langPath,
2193
+ onInstall,
2194
+ onUninstall
2195
+ }))), plugins !== null && !failed && (0, react.createElement)("div", { className: List_module_css_default.footer }, (0, react.createElement)("a", {
2196
+ className: List_module_css_default.footLink,
2197
+ href: `https://dsh-plugin.org/${langPath}`,
2198
+ target: "_blank",
2199
+ rel: "noopener noreferrer"
2200
+ }, t("browseAll", { n: total }))));
2201
+ }
2202
+ //#endregion
2203
+ //#region src/client/components/PluginHubSection.tsx
2204
+ /**
2205
+ * Plugin Hub section: wires the catalog data pipeline, the background task
2206
+ * queue and the feedback state together. Data/queue logic lives in hooks/,
2207
+ * rendering is delegated to the small presentational components in this
2208
+ * folder; only the dialogs/toast and the section-level copy actions remain.
2209
+ */
2210
+ function PluginHubSection({ t: _hostT, locale }) {
2211
+ /** 界面语言:默认跟随宿主(系统)语言;右上角按钮可手动切换,切换后以手动选择为准 */
2212
+ const [manualLang, setManualLang] = (0, react.useState)(null);
2213
+ const lang = manualLang ?? locale.getSnapshot().active;
373
2214
  const langKey = lang === "en" ? "en" : "zh";
374
- const langPath = lang === "zh" ? "zh/" : "";
2215
+ const langPath = langPathOf(lang);
375
2216
  const dict = langKey === "en" ? en : zh;
376
2217
  const t = (key, params) => {
377
2218
  const raw = dict[key] ?? key;
378
2219
  return params ? raw.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? "")) : raw;
379
2220
  };
380
- const catLabel = (map, key) => map[key]?.[langKey] ?? key;
381
- const copyInstall = async (repo) => {
382
- const text = `dsh plugin add ${repo}`;
383
- let ok = false;
2221
+ const catalog = useCatalog(lang);
2222
+ /** 全局反馈 Toast:{id} 用于重复触发时重新走入场动画,kind 决定文案与配色 */
2223
+ const [toast, setToast] = (0, react.useState)(null);
2224
+ /** 复制反馈:记录刚复制安装命令的仓库,按钮短暂切换为「已复制」样式 */
2225
+ const [copied, setCopied] = (0, react.useState)(null);
2226
+ /** 信任确认弹窗:记录待安装的插件,确认后才执行复制 */
2227
+ const [confirmPlugin, setConfirmPlugin] = (0, react.useState)(null);
2228
+ /** 弹窗动作是否为「更新」:已安装插件点「更新」→ 走同一条 add 命令原位覆盖重装,文案区分安装/更新 */
2229
+ const [confirmIsUpdate, setConfirmIsUpdate] = (0, react.useState)(false);
2230
+ /** 卸载确认弹窗:记录待卸载的插件 */
2231
+ const [uninstallPlugin, setUninstallPlugin] = (0, react.useState)(null);
2232
+ /** 安装/卸载完成后的结果视图:停留弹窗内,点「完成」关闭 */
2233
+ const [installDone, setInstallDone] = (0, react.useState)(false);
2234
+ const [uninstallDone, setUninstallDone] = (0, react.useState)(false);
2235
+ /** 结果视图「立即重启」:请求宿主重启后进入等待,服务回来后整页刷新 */
2236
+ const [restarting, setRestarting] = (0, react.useState)(false);
2237
+ /** 操作失败完整信息 + 所属插件仓库 + 失败类型(决定弹窗标题「安装失败/卸载失败」)+ 实际执行的安装命令 + 尝试过的安装方式(issue 预填用) */
2238
+ const [errorMsg, setErrorMsg] = (0, react.useState)(null);
2239
+ /** 安装/卸载任务通知记录:localStorage 持久化,成败即落盘,即使错过弹窗也能回来查看 */
2240
+ const [notifications, setNotifications] = (0, react.useState)(() => loadNotifications());
2241
+ const [showNotifications, setShowNotifications] = (0, react.useState)(false);
2242
+ /** 宿主机器环境快照:提交 bug 的 issue 正文附带;取不到为 null(链接少环境段,不阻塞) */
2243
+ const [env, setEnv] = (0, react.useState)(null);
2244
+ (0, react.useEffect)(() => {
2245
+ let alive = true;
2246
+ getEnv().then((info) => {
2247
+ if (alive) setEnv(info);
2248
+ });
2249
+ return () => {
2250
+ alive = false;
2251
+ };
2252
+ }, []);
2253
+ const queue = useTaskQueue({
2254
+ t,
2255
+ refreshInstalled: catalog.refreshInstalled,
2256
+ onInstallDone: (viaModal, repo) => {
2257
+ if (viaModal) setInstallDone(true);
2258
+ else setToast({
2259
+ id: Date.now(),
2260
+ kind: "done"
2261
+ });
2262
+ setNotifications(addSuccess({
2263
+ kind: "install",
2264
+ repo: repo ?? ""
2265
+ }));
2266
+ },
2267
+ onUninstallDone: (viaModal, repo) => {
2268
+ if (viaModal) setUninstallDone(true);
2269
+ else setToast({
2270
+ id: Date.now(),
2271
+ kind: "removed"
2272
+ });
2273
+ setNotifications(addSuccess({
2274
+ kind: "uninstall",
2275
+ repo: repo ?? ""
2276
+ }));
2277
+ },
2278
+ onError: (message, repo, kind, command, attempts) => {
2279
+ setErrorMsg({
2280
+ message,
2281
+ repo,
2282
+ kind,
2283
+ command,
2284
+ attempts
2285
+ });
2286
+ setNotifications(addFailure({
2287
+ kind,
2288
+ repo: repo ?? "",
2289
+ message,
2290
+ command,
2291
+ attempts
2292
+ }));
2293
+ },
2294
+ installPlugin: confirmPlugin,
2295
+ uninstallPlugin,
2296
+ installedName: catalog.installedName,
2297
+ resolvePending: (repo) => {
2298
+ const p = catalog.plugins?.find((x) => x.source?.repo === repo);
2299
+ return p ? {
2300
+ desc: p.description,
2301
+ version: p.version
2302
+ } : null;
2303
+ }
2304
+ });
2305
+ (0, react.useEffect)(() => {
2306
+ if (!confirmPlugin && !uninstallPlugin && !showNotifications) return;
2307
+ const onKey = (e) => {
2308
+ if (e.key === "Escape") {
2309
+ setConfirmPlugin(null);
2310
+ setUninstallPlugin(null);
2311
+ setUninstallDone(false);
2312
+ setShowNotifications(false);
2313
+ }
2314
+ };
2315
+ window.addEventListener("keydown", onKey);
2316
+ return () => window.removeEventListener("keydown", onKey);
2317
+ }, [
2318
+ confirmPlugin,
2319
+ uninstallPlugin,
2320
+ showNotifications
2321
+ ]);
2322
+ (0, react.useEffect)(() => {
2323
+ if (!toast) return;
2324
+ const timer = window.setTimeout(() => setToast(null), 2400);
2325
+ return () => window.clearTimeout(timer);
2326
+ }, [toast]);
2327
+ /** 复制文本到剪贴板,返回是否成功(Clipboard API + 隐藏 textarea 兜底)。 */
2328
+ const doCopy = async (text) => {
384
2329
  try {
385
2330
  await navigator.clipboard.writeText(text);
386
- ok = true;
2331
+ return true;
387
2332
  } catch {
388
2333
  const ta = document.createElement("textarea");
389
2334
  ta.value = text;
@@ -391,117 +2336,224 @@ window.__ModuleLoader__.load({ id: "dsh-plugin", factory: (require) => {
391
2336
  ta.style.opacity = "0";
392
2337
  document.body.appendChild(ta);
393
2338
  ta.select();
2339
+ let ok = false;
394
2340
  try {
395
2341
  ok = document.execCommand("copy");
396
2342
  } catch {
397
2343
  ok = false;
398
2344
  }
399
2345
  document.body.removeChild(ta);
2346
+ return ok;
400
2347
  }
401
- if (ok) {
2348
+ };
2349
+ /** 弹窗动作一:复制安装命令到剪贴板,引导去终端粘贴执行(npm 通道显示包名命令)。 */
2350
+ const copyCommand = async (p) => {
2351
+ const repo = p.source?.repo ?? "";
2352
+ if (await doCopy(installCommandOf(p))) {
402
2353
  setCopied(repo);
403
- setToast({ id: Date.now() });
2354
+ setToast({
2355
+ id: Date.now(),
2356
+ kind: "copied"
2357
+ });
404
2358
  window.setTimeout(() => setCopied((cur) => cur === repo ? null : cur), 1600);
405
- window.setTimeout(() => setToast(null), 1800);
406
- } else setCopied(null);
2359
+ }
2360
+ setConfirmPlugin(null);
407
2361
  };
408
- const total = plugins?.length ?? 0;
409
- const count = visible.length;
410
- const statsTotal = stats?.total ?? total;
411
- const statsVerified = stats?.verified ?? 0;
412
- const categoryCounts = (0, react.useMemo)(() => {
413
- const counts = { all: total };
414
- for (const p of plugins ?? []) if (p.category) counts[p.category] = (counts[p.category] ?? 0) + 1;
415
- return counts;
416
- }, [plugins, total]);
417
- return (0, react.createElement)("div", { className: Section_module_css_default.root }, (0, react.createElement)("div", { className: Section_module_css_default.header }, (0, react.createElement)("div", { className: Section_module_css_default.brand }, (0, react.createElement)("div", { className: Section_module_css_default.brandText }, (0, react.createElement)("h1", { className: Section_module_css_default.title }, t("title")), (0, react.createElement)("div", { className: Section_module_css_default.tagline }, t("tagline", {
418
- total: statsTotal,
419
- verified: statsVerified
420
- })))), (0, react.createElement)("div", { className: Section_module_css_default.controls }, (0, react.createElement)("input", {
421
- className: Section_module_css_default.search,
422
- type: "search",
423
- placeholder: t("search"),
424
- value: query,
425
- spellCheck: false,
426
- onInput: (e) => setQuery(e.target.value)
427
- }), (0, react.createElement)("select", {
428
- className: Section_module_css_default.sort,
429
- value: sort,
430
- onChange: (e) => setSort(e.target.value)
431
- }, SORTS.map((key) => (0, react.createElement)("option", {
432
- key,
433
- value: key
434
- }, t(key)))), (0, react.createElement)("button", {
435
- className: Section_module_css_default.langBtn,
436
- title: lang === "zh" ? t("toEn") : t("toZh"),
437
- onClick: () => setLang(lang === "zh" ? "en" : "zh")
438
- }, lang === "zh" ? "EN" : "中文"), (0, react.createElement)("a", {
439
- className: Section_module_css_default.openBtn,
440
- href: `${SITE_URL}${langPath}`,
441
- target: "_blank",
442
- rel: "noopener noreferrer",
443
- title: t("openHint")
444
- }, t("openBtn"), (0, react.createElement)("span", { className: Section_module_css_default.openArrow }, "↗")))), (0, react.createElement)("a", {
445
- className: Section_module_css_default.adBanner,
446
- href: `${SITE_URL}${langPath}`,
447
- target: "_blank",
448
- rel: "noopener noreferrer"
449
- }, (0, react.createElement)("span", { className: Section_module_css_default.adBadge }, t("adBadge")), (0, react.createElement)("span", { className: Section_module_css_default.adText }, t("ad", {
450
- total: statsTotal,
451
- verified: statsVerified
452
- })), (0, react.createElement)("span", { className: Section_module_css_default.adArrow }, "↗")), (0, react.createElement)("div", { className: Section_module_css_default.tabs }, (0, react.createElement)("button", {
453
- key: "all",
454
- className: category === "all" ? Section_module_css_default.tabActive : Section_module_css_default.tab,
455
- onClick: () => setCategory("all")
456
- }, t("all"), (0, react.createElement)("span", { className: category === "all" ? Section_module_css_default.tabCountActive : Section_module_css_default.tabCount }, total)), CATEGORY_ORDER.map((id) => (0, react.createElement)("button", {
457
- key: id,
458
- className: category === id ? Section_module_css_default.tabActive : Section_module_css_default.tab,
459
- onClick: () => setCategory(id)
460
- }, catLabel(CATEGORY_SHORT_LABELS, id), (0, react.createElement)("span", { className: category === id ? Section_module_css_default.tabCountActive : Section_module_css_default.tabCount }, categoryCounts[id] ?? 0)))), (0, react.createElement)("div", { className: Section_module_css_default.body }, (0, react.createElement)("div", {
461
- ref: listRef,
462
- className: Section_module_css_default.list
463
- }, plugins === null && !failed && (0, react.createElement)("div", { className: Section_module_css_default.state }, t("loading")), failed && (0, react.createElement)("div", { className: Section_module_css_default.state }, (0, react.createElement)("div", { className: Section_module_css_default.stateTitle }, t("failed")), (0, react.createElement)("div", { className: Section_module_css_default.stateDesc }, t("failedDesc")), (0, react.createElement)("button", {
464
- className: Section_module_css_default.retryBtn,
465
- onClick: () => setReloadKey((k) => k + 1)
466
- }, t("retry"))), plugins !== null && !failed && count === 0 && (0, react.createElement)("div", { className: Section_module_css_default.state }, (0, react.createElement)("div", { className: Section_module_css_default.stateTitle }, t("noResult")), (0, react.createElement)("div", { className: Section_module_css_default.stateDesc }, t("noResultDesc"))), plugins !== null && !failed && visible.map((p) => {
467
- const repo = p.source?.repo ?? "";
468
- const isCopied = copied === repo;
469
- return (0, react.createElement)("div", {
470
- key: p.ownerSlug ? `${p.ownerSlug}/${p.slug}` : p.slug,
471
- className: Section_module_css_default.card
472
- }, (0, react.createElement)("div", { className: Section_module_css_default.cardMain }, (0, react.createElement)("div", { className: Section_module_css_default.cardHead }, (0, react.createElement)("div", {
473
- className: Section_module_css_default.cardTitle,
474
- title: p.description ?? ""
475
- }, p.displayName ?? p.slug), p.category ? (0, react.createElement)("span", { className: Section_module_css_default.categoryBadge }, catLabel(CATEGORY_LABELS, p.category)) : null, p.compatibility?.status === "verified" ? (0, react.createElement)("span", { className: Section_module_css_default.verified }, t("verified")) : null), p.description && (langKey === "zh" || !/[\u4e00-\u9fff]/.test(p.description)) ? (0, react.createElement)("p", { className: Section_module_css_default.desc }, p.description) : null, (p.topics?.length ?? 0) > 0 ? (0, react.createElement)("div", { className: Section_module_css_default.topics }, p.topics.slice(0, 3).map((topic) => (0, react.createElement)("span", {
476
- key: topic,
477
- className: Section_module_css_default.topic
478
- }, topic))) : null), (0, react.createElement)("div", { className: Section_module_css_default.cardSide }, (0, react.createElement)("div", { className: Section_module_css_default.stats }, (0, react.createElement)("span", { className: Section_module_css_default.star }, "★ ", fmtStars(p.stats?.stargazers_count)), (0, react.createElement)("span", { className: Section_module_css_default.fork }, t("fork"), " ", fmtStars(p.stats?.forks_count)), (0, react.createElement)("span", { className: Section_module_css_default.date }, relTime(p.dates?.repoUpdatedAt, t))), repo ? (0, react.createElement)("div", { className: Section_module_css_default.actions }, (0, react.createElement)("a", {
479
- className: Section_module_css_default.detailBtn,
480
- href: `${SITE_URL}${langPath}plugins/${p.ownerSlug ?? repo.split("/")[0]?.toLowerCase() ?? ""}/${p.slug}`,
481
- target: "_blank",
482
- rel: "noopener noreferrer",
483
- title: p.slug
484
- }, t("detail")), (0, react.createElement)("button", {
485
- className: isCopied ? Section_module_css_default.installBtnCopied : Section_module_css_default.installBtn,
486
- onClick: () => copyInstall(repo)
487
- }, t("copy"))) : null));
488
- })), plugins !== null && !failed && (0, react.createElement)("div", { className: Section_module_css_default.footer }, (0, react.createElement)("span", { className: Section_module_css_default.footText }, count === total ? t("pluginsTotal", { n: total }) : t("filteredCount", { n: count })), (0, react.createElement)("a", {
489
- className: Section_module_css_default.footLink,
490
- href: `${SITE_URL}${langPath}`,
491
- target: "_blank",
492
- rel: "noopener noreferrer"
493
- }, t("browseAll", { n: total })))), toast && (0, react.createElement)("div", {
494
- key: toast.id,
495
- className: Section_module_css_default.toast
496
- }, t("toastCopied")));
2362
+ /** 卸载弹窗动作:复制卸载命令,万一直接卸载失败可去终端手动执行。 */
2363
+ const copyUninstallCommand = async () => {
2364
+ const name = uninstallPlugin ? catalog.installedName(uninstallPlugin) : null;
2365
+ if (!name) return;
2366
+ if (await doCopy(`dsh plugin remove ${name}`)) setToast({
2367
+ id: Date.now(),
2368
+ kind: "copied"
2369
+ });
2370
+ };
2371
+ /** 「立即重启」:POST 同源 /restart,宿主进程自杀重启;随后轮询服务恢复后整页刷新。 */
2372
+ const requestRestart = async () => {
2373
+ if (restarting) return;
2374
+ setRestarting(true);
2375
+ try {
2376
+ await fetch("/dsh-plugin-hub/restart", { method: "POST" });
2377
+ } catch {}
2378
+ let attempts = 0;
2379
+ const poll = async () => {
2380
+ attempts += 1;
2381
+ try {
2382
+ if ((await fetch("/dsh-plugin-hub/active", {
2383
+ method: "GET",
2384
+ cache: "no-store"
2385
+ })).ok) {
2386
+ window.location.reload();
2387
+ return;
2388
+ }
2389
+ } catch {}
2390
+ if (attempts < 60) window.setTimeout(poll, 1e3);
2391
+ else {
2392
+ setRestarting(false);
2393
+ setToast({
2394
+ id: Date.now(),
2395
+ kind: "fail"
2396
+ });
2397
+ }
2398
+ };
2399
+ window.setTimeout(poll, 1200);
2400
+ };
2401
+ const { total, installedName, installedVersion, hasUpdate } = catalog;
2402
+ const statsTotal = catalog.stats?.total ?? total;
2403
+ const statsVerified = catalog.stats?.verified ?? 0;
2404
+ const count = catalog.visible.length;
2405
+ return (0, react.createElement)("div", { className: Header_module_css_default.root }, (0, react.createElement)(CatalogHeader, {
2406
+ t,
2407
+ langPath,
2408
+ statsTotal,
2409
+ statsVerified,
2410
+ onToggleLang: () => setManualLang(lang === "en" ? "zh" : "en")
2411
+ }), (0, react.createElement)(CategoryTabs, {
2412
+ category: catalog.category,
2413
+ setCategory: catalog.setCategory,
2414
+ allLabel: t("all"),
2415
+ totalCount: catalog.total,
2416
+ langKey
2417
+ }), (0, react.createElement)(CatalogControls, {
2418
+ query: catalog.query,
2419
+ setQuery: catalog.setQuery,
2420
+ sort: catalog.sort,
2421
+ setSort: catalog.setSort,
2422
+ installedFilter: catalog.installedFilter,
2423
+ setInstalledFilter: catalog.setInstalledFilter,
2424
+ installedCount: catalog.installedCountInCategory,
2425
+ notInstalledCount: catalog.notInstalledCountInCategory,
2426
+ t,
2427
+ resultText: catalog.plugins === null || catalog.failed ? null : catalog.category === "all" && count === total ? t("pluginsTotal", { n: count }) : t("filterResults", { n: count }),
2428
+ noticeCount: notifications.length + queue.queue.length + queue.pendingRestarts.length,
2429
+ onOpenNotifications: () => setShowNotifications(true)
2430
+ }), (0, react.createElement)(CatalogList, {
2431
+ plugins: catalog.plugins,
2432
+ failed: catalog.failed,
2433
+ visible: catalog.visible,
2434
+ total,
2435
+ t,
2436
+ langPath,
2437
+ reload: catalog.reload,
2438
+ category: catalog.category,
2439
+ installedFilter: catalog.installedFilter,
2440
+ copied,
2441
+ installedName,
2442
+ installedVersion,
2443
+ hasUpdate,
2444
+ langKey,
2445
+ onInstall: (p, opts) => {
2446
+ setInstallDone(false);
2447
+ setConfirmIsUpdate(opts?.update ?? false);
2448
+ queue.clearModalTask();
2449
+ setConfirmPlugin(p);
2450
+ },
2451
+ onUninstall: (p) => {
2452
+ setUninstallDone(false);
2453
+ queue.clearModalTask();
2454
+ setUninstallPlugin(p);
2455
+ }
2456
+ }), confirmPlugin && (0, react.createElement)(InstallModal, {
2457
+ plugin: confirmPlugin,
2458
+ done: installDone,
2459
+ task: queue.installModalTask,
2460
+ t,
2461
+ langPath,
2462
+ restarting,
2463
+ update: confirmIsUpdate,
2464
+ submitting: queue.submitting,
2465
+ onClose: () => setConfirmPlugin(null),
2466
+ onCopy: () => copyCommand(confirmPlugin),
2467
+ onInstall: () => queue.installNow(confirmPlugin, confirmIsUpdate ? { update: true } : void 0),
2468
+ onRestart: () => {
2469
+ requestRestart();
2470
+ }
2471
+ }), uninstallPlugin && (0, react.createElement)(UninstallModal, {
2472
+ plugin: uninstallPlugin,
2473
+ done: uninstallDone,
2474
+ task: queue.uninstallModalTask,
2475
+ t,
2476
+ langPath,
2477
+ restarting,
2478
+ submitting: queue.submitting,
2479
+ onClose: () => {
2480
+ setUninstallDone(false);
2481
+ setUninstallPlugin(null);
2482
+ },
2483
+ onCancel: () => setUninstallPlugin(null),
2484
+ onCopyCommand: copyUninstallCommand,
2485
+ onConfirm: () => queue.uninstallNow(uninstallPlugin),
2486
+ onRestart: () => {
2487
+ requestRestart();
2488
+ }
2489
+ }), toast && (0, react.createElement)(Toast, {
2490
+ toast,
2491
+ t
2492
+ }), errorMsg && (0, react.createElement)(ErrorModal, {
2493
+ message: errorMsg.message,
2494
+ repo: errorMsg.repo,
2495
+ kind: errorMsg.kind,
2496
+ command: errorMsg.command,
2497
+ attempts: errorMsg.attempts,
2498
+ t,
2499
+ env,
2500
+ onCopy: (text) => {
2501
+ doCopy(text);
2502
+ setToast({
2503
+ id: Date.now(),
2504
+ kind: "errCopied"
2505
+ });
2506
+ },
2507
+ onClose: () => setErrorMsg(null)
2508
+ }), showNotifications && (0, react.createElement)(NotificationsModal, {
2509
+ records: notifications,
2510
+ tasks: queue.queue,
2511
+ pendingRestarts: queue.pendingRestarts,
2512
+ t,
2513
+ env,
2514
+ onClose: () => setShowNotifications(false),
2515
+ onCopy: (text) => {
2516
+ doCopy(text);
2517
+ setToast({
2518
+ id: Date.now(),
2519
+ kind: "errCopied"
2520
+ });
2521
+ },
2522
+ onClear: () => setNotifications(clearNotifications()),
2523
+ onRemove: (id) => setNotifications(removeNotification(id)),
2524
+ cancelTask: queue.cancelTask,
2525
+ restarting,
2526
+ onRestart: () => {
2527
+ requestRestart();
2528
+ }
2529
+ }));
497
2530
  }
2531
+ //#endregion
2532
+ //#region \0dsh-css:src/client/styles/tokens.module.css.mjs
2533
+ const css = ":root,body[data-ds-dark-theme]{--hub-text-primary:var(--dsw-alias-label-primary,#1f2328);--hub-text-secondary:var(--dsw-alias-label-secondary,#6b7280);--hub-text-tertiary:var(--dsw-alias-label-tertiary,#8b93a1);--hub-text-disabled:var(--dsw-alias-label-disabled,#aab1bd);--hub-text-on-fill:var(--dsw-alias-label-primary-foreground,#fff);--hub-bg-1:var(--dsw-alias-bg-layer-1,#fff);--hub-bg-2:var(--dsw-alias-bg-layer-2,#80808014);--hub-bg-3:#8080801a;--hub-bg-btn:#8080801f;--hub-bg-hover:var(--dsw-alias-interactive-bg-hover,#80808029);--hub-bg-on-fill:#80808038;--hub-border-1:var(--dsw-alias-border-l1,#eceef1);--hub-border-2:var(--dsw-alias-border-l2,#e5e7eb);--hub-border-ghost:var(--dsw-alias-button-ghost-active-border,#4f6ef740);--hub-border-input:var(--dsw-alias-input-border,#6b728040);--hub-brand:var(--dsw-alias-state-business-primary,#4f6ef7);--hub-brand-hover:#3b5bdb;--hub-brand-tint:#4f6ef70f;--hub-brand-tint-strong:#4f6ef71f;--hub-brand-border:#4f6ef759;--hub-brand-border-soft:#4f6ef738;--hub-purple-1:#4f46e5;--hub-purple-2:#7c3aed;--hub-purple-border:#8b5cf68c;--hub-purple-tint:#4f46e514;--hub-purple-shadow:#4f46e547;--hub-purple-shadow-strong:#7c3aed6b;--hub-btn-fill:var(--dsw-alias-button-primary-fill,#1f2328);--hub-btn-hover:var(--dsw-alias-button-primary-hover,#43454a);--hub-success:#16a34a;--hub-success-tint:#16a34a1f;--hub-success-border:#16a34a4d;--hub-warn:var(--dsw-alias-state-warn-primary,#b8860b);--hub-warn-tint:#b8860b14;--hub-warn-border:#b8860b59;--hub-warn-strong:#b8860bd9;--hub-warning:var(--dsw-alias-state-warning-primary,#b45309);--hub-warning-tint:#b4530914;--hub-warning-border:#b453094d;--hub-danger:var(--dsw-alias-state-danger-primary,#d1242f);--hub-danger-hover:var(--dsw-alias-state-danger-hover,#b91c1c);--hub-danger-strong:#b0202a;--hub-danger-text:#e5484d;--hub-danger-tint:#e5484d14;--hub-danger-tint-weak:#e5484d0d;--hub-danger-border:#e5484d59;--hub-danger-border-soft:#e5484d47;--hub-danger-soft:#d1242f14}body[data-ds-dark-theme]{--hub-bg-btn:#ffffff1f;--hub-success:#22c55e;--hub-success-tint:#22c55e29;--hub-success-border:#22c55e59}";
2534
+ const tagId = "dsh-plugin/tokens.module.css";
2535
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
2536
+ const tag = document.createElement("style");
2537
+ tag.dataset.plugin = "dsh-plugin";
2538
+ tag.dataset.pluginCss = tagId;
2539
+ tag.textContent = css;
2540
+ document.head.appendChild(tag);
2541
+ }
2542
+ //#endregion
2543
+ //#region src/client/index.tsx
2544
+ /**
2545
+ * dsh-plugin client entry: wires the Plugin Hub into the host settings via
2546
+ * the module-loader bundle (see tsdown.config.ts). All UI logic lives in
2547
+ * PluginHubSection; this file only performs the cordis apply wiring.
2548
+ */
2549
+ const NS = "dsh-plugin";
498
2550
  const name = NS;
499
2551
  const inject = ["slots", "locale"];
500
2552
  function apply(ctx) {
501
2553
  ctx.effect(() => ctx.locale.register(NS, {
502
2554
  zh,
503
2555
  en
504
- }), "dsh-plugin: dictionaries");
2556
+ }), `${NS}: dictionaries`);
505
2557
  const t = ctx.locale.bind(NS);
506
2558
  ctx.slots.inject("settings.section", () => ctx.slots.register({
507
2559
  name: "settings.section",