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