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