@1e0zj/dsh-plugin-mall 0.1.16 → 0.1.18

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/src/client.js CHANGED
@@ -1,610 +1,694 @@
1
- // Browser half of dsh-plugin-mall: the Settings → Plugins → 插件市场 tab.
2
- //
3
- // Client plugin contract (see @deepseek-ai/dsh-client-modules): the bundle
4
- // registers itself on window.__ModuleLoader__, exports a cordis-style
5
- // { apply, inject }, and is served at /plugins/@1e0zj/dsh-plugin-mall/client.js.
6
- // This file is hand-written ES5-ish JS on purpose — no build step — using the
7
- // React instance the shell shares through the client module loader.
8
- window.__ModuleLoader__.load({
9
- id: "@1e0zj/dsh-plugin-mall",
10
- factory: (require) => {
11
- var module = { exports: {} };
12
- var exports = module.exports;
13
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
14
- var React = require("react");
15
-
16
- // ── styles ──────────────────────────────────────────────────────────────
17
- var css = [
18
- ".mkt_root{display:flex;flex-direction:column;gap:12px;max-width:1040px}",
19
- ".mkt_head{display:flex;flex-direction:column;gap:2px}",
20
- ".mkt_title{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary)}",
21
- ".mkt_sub{font-size:12px;color:var(--dsw-alias-label-tertiary)}",
22
- ".mkt_row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}",
23
- ".mkt_input{flex:1;min-width:200px;background:var(--dsw-alias-bg-secondary,#fff);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:6px 10px;font-size:13px;color:var(--dsw-alias-label-primary)}",
24
- ".mkt_select{background:var(--dsw-alias-bg-secondary,#fff);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:6px 8px;font-size:13px;color:var(--dsw-alias-label-primary)}",
25
- ".mkt_btn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-secondary,#fff);color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 12px;font-size:13px;cursor:pointer}",
26
- ".mkt_btn:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}",
27
- ".mkt_btn:disabled{opacity:.55;cursor:default}",
28
- ".mkt_btnPrimary{background:var(--dsw-alias-state-business-primary,#2b6cb0);border-color:transparent;color:#fff}",
29
- ".mkt_btnPrimary:hover:not(:disabled){color:#fff;opacity:.92}",
30
- ".mkt_btnDanger:hover:not(:disabled){border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}",
31
- ".mkt_btnSm{padding:3px 10px;font-size:12px}",
32
- ".mkt_error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}",
33
- ".mkt_list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;align-items:stretch;min-width:0}",
34
- "@media (max-width:820px){.mkt_list{grid-template-columns:minmax(0,1fr)}}",
35
- ".mkt_listHead{grid-column:1/-1;font-size:12px;color:var(--dsw-alias-label-tertiary)}",
36
- ".mkt_loadMore{grid-column:1/-1;text-align:center;font-size:12px;color:var(--dsw-alias-label-tertiary);padding:10px 0}",
37
- ".mkt_card{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px 12px;background:var(--dsw-alias-bg-primary,#fff);display:flex;flex-direction:column;gap:6px;min-width:0}",
38
- ".mkt_cardHead{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}",
39
- ".mkt_metaRow{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}",
40
- ".mkt_name{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}",
41
- ".mkt_meta{font-size:12px;color:var(--dsw-alias-label-tertiary)}",
42
- ".mkt_desc{font-size:12.5px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
43
- ".mkt_cardActions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:auto;padding-top:4px}",
44
- ".mkt_cardActions .mkt_btn{text-decoration:none}",
45
- ".mkt_panelTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}",
46
- ".mkt_panelRow{display:flex;align-items:center;gap:8px}",
47
- ".mkt_panelRow .mkt_link{margin-left:auto}",
48
- ".mkt_pre{font-family:Consolas,Monaco,monospace;font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-secondary,#f6f7f8);border-radius:6px;padding:8px;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-all}",
49
- ".mkt_ok{color:var(--dsw-alias-state-success-primary,#2f855a)}",
50
- ".mkt_badge{display:inline-block;border-radius:999px;padding:1px 8px;font-size:11px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary)}",
51
- ".mkt_badgeOk{border-color:var(--dsw-alias-state-success-primary,#2f855a);color:var(--dsw-alias-state-success-primary,#2f855a)}",
52
- ".mkt_badgeBad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}",
53
- ".mkt_check{display:flex;align-items:center;gap:4px;font-size:12.5px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap}",
54
- ".mkt_link{color:var(--dsw-alias-state-business-primary);font-size:12px;text-decoration:none;cursor:pointer}",
55
- ".mkt_installedHead{display:flex;align-items:center;gap:8px;width:100%;background:none;border:0;padding:0;margin:0;cursor:pointer;font:inherit;text-align:left}",
56
- ".mkt_installedHead:hover .mkt_panelTitle{color:var(--dsw-alias-state-business-primary)}",
57
- ".mkt_installedToggle{margin-left:auto}",
58
- ".mkt_depList{display:flex;flex-direction:column;gap:6px;margin-top:8px}",
59
- ".mkt_depRow{display:flex;justify-content:space-between;align-items:center;gap:8px}",
60
- ".mkt_depActions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}",
61
- ].join("\n");
62
- var tagId = "@1e0zj/dsh-plugin-mall/market-tab.css";
63
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
64
- var styleTag = document.createElement("style");
65
- styleTag.dataset.plugin = "@1e0zj/dsh-plugin-mall";
66
- styleTag.dataset.pluginCss = tagId;
67
- styleTag.textContent = css;
68
- document.head.appendChild(styleTag);
69
- }
70
-
71
- // ── tiny helpers ────────────────────────────────────────────────────────
72
- var h = React.createElement;
73
- var useState = React.useState;
74
- var useEffect = React.useEffect;
75
- var useRef = React.useRef;
76
- var useCallback = React.useCallback;
77
-
78
- function clip(text, max) {
79
- var t = String(text == null ? "" : text).replace(/\s+/g, " ").trim();
80
- return t.length > max ? t.slice(0, max - 1) + "…" : t;
81
- }
82
-
83
- function errorText(e) {
84
- return String(e && e.message ? e.message : e);
85
- }
86
-
87
- function kindLabel(kind) {
88
- if (kind === "bundle") return "宿主插件层";
89
- if (kind === "client") return "浏览器UI插件";
90
- if (kind === "missing") return "未解析";
91
- return "普通依赖";
92
- }
93
-
94
- // ── job polling ─────────────────────────────────────────────────────────
95
- function useJobPolling(call, onSettled) {
96
- var jobsRef = useRef({});
97
- var _jobs = useState({});
98
- var jobs = _jobs[0];
99
- var setJobs = _jobs[1];
100
- useEffect(function () {
101
- var timer = setInterval(function () {
102
- var current = jobsRef.current;
103
- var running = Object.keys(current).filter(function (id) {
104
- var job = current[id];
105
- return job && job.status !== "completed" && job.status !== "failed" && job.status !== "killed";
106
- });
107
- if (running.length === 0) return;
108
- running.forEach(function (id) {
109
- call("job", { jobId: id }).then(function (value) {
110
- var snapshot = value.snapshot || {};
111
- var old = jobsRef.current[id] || {};
112
- var wasTerminal = old.status === "completed" || old.status === "failed" || old.status === "killed";
113
- var nowTerminal = snapshot.status === "completed" || snapshot.status === "failed" || snapshot.status === "killed";
114
- if (!wasTerminal && nowTerminal && onSettled) {
115
- try { onSettled(id); } catch (e) { /* best effort */ }
116
- }
117
- var output = (old.output || "") + (value.output || "");
118
- jobsRef.current = Object.assign({}, jobsRef.current, { [id]: Object.assign({}, old, {
119
- status: snapshot.status,
120
- detail: snapshot.detail,
121
- output: output,
122
- }) });
123
- setJobs(Object.assign({}, jobsRef.current));
124
- }).catch(function () { /* keep polling */ });
125
- });
126
- }, 1200);
127
- return function () { clearInterval(timer); };
128
- }, [call, onSettled]);
129
- var track = useCallback(function (id, spec) {
130
- jobsRef.current = Object.assign({}, jobsRef.current, { [id]: { status: "running", spec: spec, output: "" } });
131
- setJobs(Object.assign({}, jobsRef.current));
132
- }, []);
133
- var clear = useCallback(function () {
134
- jobsRef.current = {};
135
- setJobs({});
136
- }, []);
137
- return { jobs: jobs, track: track, clear: clear };
138
- }
139
-
140
- // ── plugin verification badge ───────────────────────────────────────────
141
- // verified 来自 /market verify 端点(node 侧拉 raw package.json 判定
142
- // dsh.bundle/dsh.client 声明,进程内缓存)。unknown 不显示徽章。
143
- function verifyBadge(verified) {
144
- if (verified === undefined || verified === null) return null;
145
- if (verified.hostDeps !== undefined && verified.hostDeps.length > 0) {
146
- return h("span", { className: "mkt_badge mkt_badgeBad", title: verified.hostDeps.join(", ") }, "宿主依赖风险");
147
- }
148
- if (verified.kind === "bundle") return h("span", { className: "mkt_badge mkt_badgeOk" }, "宿主插件");
149
- if (verified.kind === "client") return h("span", { className: "mkt_badge mkt_badgeOk" }, "UI插件");
150
- if (verified.kind === "plain") return h("span", { className: "mkt_badge" }, "未声明");
151
- if (verified.kind === "no-manifest") return h("span", { className: "mkt_badge" }, "无package.json");
152
- return null;
153
- }
154
-
155
- // ── repo card ───────────────────────────────────────────────────────────
156
- function RepoCard(props) {
157
- var item = props.item;
158
- var installing = props.installing === true;
159
- var job = props.installJob;
160
- var installLabel = props.alreadyInstalled === true ? "已装" : "安装";
161
- var installDisabled = installing || props.alreadyInstalled === true;
162
- if (!installing && job) {
163
- if (job.status === "running") { installLabel = "安装中…"; installDisabled = true; }
164
- else if (job.status === "completed") { installLabel = "已装 · 重启生效"; installDisabled = true; }
165
- else if (job.status === "failed") { installLabel = "安装失败 · 见任务日志"; }
166
- else { installLabel = "已取消 · 重试"; }
167
- }
168
- return h("div", { className: "mkt_card" },
169
- h("div", { className: "mkt_cardHead" },
170
- h("span", { className: "mkt_name" }, item.fullName),
171
- verifyBadge(props.verified),
172
- item.archived ? h("span", { className: "mkt_badge" }, "archived") : null
173
- ),
174
- item.description ? h("div", { className: "mkt_desc" }, item.description) : null,
175
- h("div", { className: "mkt_metaRow" },
176
- h("span", { className: "mkt_meta" }, "★" + item.stars),
177
- item.language ? h("span", { className: "mkt_meta" }, item.language) : null,
178
- item.license ? h("span", { className: "mkt_meta" }, item.license) : null,
179
- h("span", { className: "mkt_meta" }, "更新 " + (item.updatedAt || "").slice(0, 10))
180
- ),
181
- h("div", { className: "mkt_cardActions" },
182
- h("button", { className: "mkt_btn mkt_btnPrimary", disabled: installDisabled, onClick: function () { props.onInstall(item.fullName); } },
183
- installLabel),
184
- h("a", { className: "mkt_btn", href: item.htmlUrl, target: "_blank", rel: "noreferrer" }, "前往仓库")
185
- )
186
- );
187
- }
188
-
189
- // ── jobs panel ──────────────────────────────────────────────────────────
190
- function JobsPanel(props) {
191
- var ids = Object.keys(props.jobs);
192
- if (ids.length === 0) return null;
193
- return h("div", { className: "mkt_card" },
194
- h("div", { className: "mkt_panelRow" },
195
- h("p", { className: "mkt_panelTitle" }, "任务"),
196
- props.onClear ? h("span", { className: "mkt_link", onClick: props.onClear }, "清空") : null
197
- ),
198
- ids.map(function (id) {
199
- var job = props.jobs[id];
200
- var done = job.status === "completed" || job.status === "failed" || job.status === "killed";
201
- return h("div", { key: id, style: { display: "flex", flexDirection: "column", gap: "4px" } },
202
- h("div", { className: "mkt_meta" },
203
- id + " · " + clip(job.spec || "", 40) + " · " + (job.status || "running")),
204
- job.detail ? h("div", { className: "mkt_desc" }, job.detail) : null,
205
- done && job.status === "completed"
206
- ? h("div", { className: "mkt_ok" }, "完成 — 重启 dsh 后生效")
207
- : job.status === "failed"
208
- ? h("div", { className: "mkt_error" }, "失败,见下方输出")
209
- : null,
210
- job.output ? h("pre", { className: "mkt_pre" }, job.output.slice(-4000)) : null
211
- );
212
- })
213
- );
214
- }
215
-
216
- // ── installed panel (collapsible summary) ───────────────────────────────
217
- function InstalledPanel(props) {
218
- var installed = props.installed;
219
- var _open = useState(false);
220
- var open = _open[0];
221
- var setOpen = _open[1];
222
- if (!installed) return null;
223
- var count = installed.error ? 0 : (installed.deps || []).length;
224
- return h("div", { className: "mkt_card" },
225
- h("button", {
226
- type: "button",
227
- className: "mkt_installedHead",
228
- onClick: function () { setOpen(!open); },
229
- "aria-expanded": open ? "true" : "false",
230
- },
231
- h("span", { className: "mkt_panelTitle" }, "已装插件"),
232
- h("span", { className: "mkt_badge" }, count + " 个"),
233
- h("span", { className: "mkt_meta mkt_installedToggle" }, open ? "收起" : "展开")
234
- ),
235
- installed.error
236
- ? h("div", { className: "mkt_error" }, installed.error)
237
- : !open ? null
238
- : (installed.deps || []).length === 0
239
- ? h("div", { className: "mkt_meta" }, "还没有装过插件")
240
- : h("div", { className: "mkt_depList" }, (installed.deps || []).map(function (dep) {
241
- var busy = (props.removing || {})[dep.name] === true;
242
- var upd = (props.updates || {})[dep.name];
243
- return h("div", { key: dep.name, className: "mkt_depRow" },
244
- h("span", { className: "mkt_desc" }, dep.name + "@" + dep.version),
245
- h("span", { className: "mkt_depActions" },
246
- h("span", { className: "mkt_badge" }, kindLabel(dep.kind)),
247
- // 必须带上版本号。传裸包名等于让 pnpm 自己挑"最新可安装
248
- // 版本",而 pnpm 11 minimumReleaseAge 会拒绝发布不足
249
- // 24 小时的版本并回落到旧版 —— 按钮写着"更新至 0.12.3",
250
- // pnpm 却报 "Already up to date",点多少次都不动。
251
- // 带上版本是明确指定,pnpm 照装并自行记一条豁免。
252
- upd && upd.hasUpdate ? h("button", {
253
- className: "mkt_btn mkt_btnSm",
254
- onClick: function () { props.onInstallSpec(dep.name + "@" + upd.latest); },
255
- }, "更新至 " + upd.latest) : null,
256
- h("button", {
257
- className: "mkt_btn mkt_btnDanger mkt_btnSm",
258
- disabled: busy,
259
- onClick: function () { props.onUninstall(dep.name); },
260
- }, busy ? "卸载中…" : "卸载")));
261
- }))
262
- );
263
- }
264
-
265
- // ── the marketplace tab ─────────────────────────────────────────────────
266
- function MarketplaceTab(props) {
267
- var rpc = props.rpc;
268
- var _query = useState("");
269
- var query = _query[0];
270
- var setQuery = _query[1];
271
- var _sort = useState("stars");
272
- var sort = _sort[0];
273
- var setSort = _sort[1];
274
- var _results = useState(null);
275
- var results = _results[0];
276
- var setResults = _results[1];
277
- var _loading = useState(false);
278
- var loading = _loading[0];
279
- var setLoading = _loading[1];
280
- var _error = useState(null);
281
- var error = _error[0];
282
- var setError = _error[1];
283
- var _installed = useState(null);
284
- var installed = _installed[0];
285
- var setInstalled = _installed[1];
286
- var _installing = useState({});
287
- var installing = _installing[0];
288
- var setInstalling = _installing[1];
289
- var _removing = useState({});
290
- var removing = _removing[0];
291
- var setRemoving = _removing[1];
292
- var _page = useState(1);
293
- var page = _page[0];
294
- var setPage = _page[1];
295
- var _loadingMore = useState(false);
296
- var loadingMore = _loadingMore[0];
297
- var setLoadingMore = _loadingMore[1];
298
- var sentinelRef = useRef(null);
299
- var _verified = useState({});
300
- var verified = _verified[0];
301
- var setVerified = _verified[1];
302
- var _verifiedOnly = useState(true);
303
- var verifiedOnly = _verifiedOnly[0];
304
- var setVerifiedOnly = _verifiedOnly[1];
305
- var _updates = useState(null);
306
- var updates = _updates[0];
307
- var setUpdates = _updates[1];
308
- var _reachedLimit = useState(false);
309
- var reachedLimit = _reachedLimit[0];
310
- var setReachedLimit = _reachedLimit[1];
311
- var _retryAt = useState(0);
312
- var retryAt = _retryAt[0];
313
- var setRetryAt = _retryAt[1];
314
- var _restarting = useState(false);
315
- var restarting = _restarting[0];
316
- var setRestarting = _restarting[1];
317
-
318
- var call = useCallback(function (endpoint, payload) {
319
- return rpc.call("/market", endpoint, payload || {}).then(function (res) {
320
- if (!res || res.ok !== true) {
321
- var err = res && res.error;
322
- var msg = err && typeof err === "object" ? (err.message || JSON.stringify(err)) : (err || "market request failed");
323
- throw new Error(msg);
324
- }
325
- return res.value;
326
- });
327
- }, [rpc]);
328
-
329
- // 每页搜索结果到货后批量验证(node 侧 raw CDN,带缓存),失败静默——
330
- // 徽章缺失可接受,不该打断浏览。
331
- var verifyPage = useCallback(function (items) {
332
- var repos = (items || []).map(function (it) { return it.fullName; });
333
- if (repos.length === 0) return;
334
- call("verify", { repos: repos }).then(function (value) {
335
- setVerified(function (prev) { return Object.assign({}, prev, value.results); });
336
- }).catch(function () { /* 徽章缺失即可 */ });
337
- }, [call]);
338
-
339
- var doSearch = useCallback(function () {
340
- setLoading(true);
341
- setError(null);
342
- setPage(1);
343
- setReachedLimit(false);
344
- setRetryAt(0);
345
- call("search", { query: query, sort: sort, perPage: 20 }).then(function (value) {
346
- setResults(value);
347
- setVerified({});
348
- verifyPage(value.items);
349
- }).catch(function (e) {
350
- setError(errorText(e));
351
- }).finally(function () {
352
- setLoading(false);
353
- });
354
- }, [call, query, sort, verifyPage]);
355
-
356
- // 无限滚动:哨兵进入视口时拉下一页并追加。GitHub topic 的翻页间数据
357
- // 可能移动造成重复,按 fullName 去重;已显示数追上 total 即到底。
358
- var canLoadMore = results !== null && results.items.length < results.total && !reachedLimit;
359
- var loadMoreLock = useRef(false);
360
- var loadMore = useCallback(function () {
361
- if (!canLoadMore || loading || loadingMore) return;
362
- // 同一 tick 内 observer 可能连续触发两次,state 更新不阻塞闭包读值,
363
- // ref 做重入闸。
364
- if (loadMoreLock.current) return;
365
- // 限流熔断:失败后 60s 内不再自动请求(哨兵重渲染会反复触发
366
- // IntersectionObserver,不熔断会连环 500 直到限流窗口过去)。
367
- if (Date.now() < retryAt) return;
368
- loadMoreLock.current = true;
369
- setLoadingMore(true);
370
- call("search", { query: query, sort: sort, perPage: 20, page: page + 1 }).then(function (value) {
371
- if (value.truncated === true) { setReachedLimit(true); return; }
372
- setPage(page + 1);
373
- verifyPage(value.items);
374
- setResults(function (prev) {
375
- if (prev === null) return value;
376
- var seen = {};
377
- var merged = [];
378
- prev.items.concat(value.items).forEach(function (it) {
379
- if (seen[it.fullName] === true) return;
380
- seen[it.fullName] = true;
381
- merged.push(it);
382
- });
383
- return { total: value.total, items: merged };
384
- });
385
- }).catch(function (e) {
386
- setError(errorText(e));
387
- setRetryAt(Date.now() + 60000);
388
- }).finally(function () {
389
- loadMoreLock.current = false;
390
- setLoadingMore(false);
391
- });
392
- }, [call, canLoadMore, loading, loadingMore, page, query, sort, retryAt]);
393
-
394
- useEffect(function () {
395
- var node = sentinelRef.current;
396
- if (node === null || node === undefined) return undefined;
397
- if (typeof IntersectionObserver === "undefined") return undefined;
398
- var observer = new IntersectionObserver(function (entries) {
399
- for (var index = 0; index < entries.length; index++) {
400
- if (entries[index].isIntersecting) { loadMore(); break; }
401
- }
402
- }, { rootMargin: "300px" });
403
- observer.observe(node);
404
- return function () { observer.disconnect(); };
405
- }, [loadMore]);
406
-
407
- var refreshInstalled = useCallback(function () {
408
- call("installed", {}).then(function (value) {
409
- setInstalled(value);
410
- call("updates", {}).then(setUpdates).catch(function () { setUpdates(null); });
411
- }).catch(function (e) {
412
- setInstalled({ error: errorText(e) });
413
- });
414
- }, [call]);
415
-
416
- // 点开即自动搜索(空关键词 = 最热插件),安装任务结束自动刷新已装列表。
417
- var polling = useJobPolling(call, refreshInstalled);
418
- var track = polling.track;
419
- var jobs = polling.jobs;
420
- var clearJobs = polling.clear;
421
-
422
- useEffect(function () {
423
- doSearch();
424
- // eslint-disable-next-line react-hooks/exhaustive-deps
425
- }, []);
426
-
427
- // 通用安装入口:spec 可以是 github:owner/repo(卡片按钮)或 npm 包名
428
- // (已装面板的更新按钮)。后端会把同源发布的 github spec 改写为 npm
429
- // tarball 安装(更快),job 里记录的是最终 spec。
430
- var doInstallSpec = useCallback(function (spec) {
431
- setInstalling(function (prev) {
432
- var next = Object.assign({}, prev, { [spec]: true });
433
- return next;
434
- });
435
- setError(null);
436
- call("install", { spec: spec }).then(function (value) {
437
- track(value.jobId, spec);
438
- }).catch(function (e) {
439
- setError(errorText(e));
440
- }).finally(function () {
441
- setInstalling(function (prev) {
442
- var next = Object.assign({}, prev);
443
- delete next[spec];
444
- return next;
445
- });
446
- });
447
- }, [call, track]);
448
-
449
- var doInstall = useCallback(function (repo) {
450
- doInstallSpec("github:" + repo);
451
- }, [doInstallSpec]);
452
-
453
- // 一键重启:loopback-only 的 /market restart 端点会 detached 拉起新的
454
- // dsh 进程后退出当前进程;这里轮询 host 恢复后自动刷新页面。
455
- var doRestart = useCallback(function () {
456
- if (typeof window !== "undefined" && typeof window.confirm === "function") {
457
- if (window.confirm("重启 dsh?正在进行的任务会中断。") !== true) return;
458
- }
459
- setRestarting(true);
460
- setError(null);
461
- call("restart", {}).then(function () {
462
- var tries = 0;
463
- var ping = setInterval(function () {
464
- tries++;
465
- if (tries > 40) {
466
- clearInterval(ping);
467
- setRestarting(false);
468
- setError("2 分钟内未检测到 dsh 重启完成,请手动检查 dsh 状态后刷新页面。");
469
- return;
470
- }
471
- rpc.call("/market", "installed", {}).then(function () {
472
- clearInterval(ping);
473
- window.location.reload();
474
- }).catch(function () { /* host 还在重启,继续等 */ });
475
- }, 3000);
476
- }).catch(function (e) {
477
- setRestarting(false);
478
- setError(errorText(e));
479
- });
480
- }, [call, rpc]);
481
-
482
- var doUninstall = useCallback(function (name) {
483
- setRemoving(function (prev) {
484
- var next = Object.assign({}, prev, { [name]: true });
485
- return next;
486
- });
487
- setError(null);
488
- call("uninstall", { package: name }).then(function (value) {
489
- track(value.jobId, name);
490
- }).catch(function (e) {
491
- setError(errorText(e));
492
- }).finally(function () {
493
- setRemoving(function (prev) {
494
- var next = Object.assign({}, prev);
495
- delete next[name];
496
- return next;
497
- });
498
- });
499
- }, [call, track]);
500
-
501
- useEffect(function () {
502
- refreshInstalled();
503
- }, [refreshInstalled]);
504
-
505
- // 任务面板位置固定(已装面板之后):不随任务状态在顶部/底部之间
506
- // 跳——npm 安装几秒即完成,"完成即落底"曾让结果凭空消失。
507
- // "只看已验证"开关下的可见项:verify 判定 bundle/client 的才算插件。
508
- // verifyPending:当前加载的仓库里还有未验证完的(verified map 尚未覆盖)。
509
- var visibleItems = results === null ? [] : results.items.filter(function (it) {
510
- if (verifiedOnly !== true) return true;
511
- var v = verified[it.fullName];
512
- return v !== undefined && (v.kind === "bundle" || v.kind === "client")
513
- && !(v.hostDeps !== undefined && v.hostDeps.length > 0);
514
- });
515
- var verifyPending = results !== null && results.items.some(function (it) { return verified[it.fullName] === undefined; });
516
-
517
- return h("div", { className: "mkt_root" },
518
- h("div", { className: "mkt_head" },
519
- h("div", { className: "mkt_title" }, "插件市场"),
520
- h("div", { className: "mkt_sub" }, "搜索 GitHub dsh-plugin 话题插件 · 装/卸后需重启 dsh 生效")
521
- ),
522
- h("div", { className: "mkt_row" },
523
- h("input", {
524
- className: "mkt_input",
525
- placeholder: "搜索插件(留空列出最热)",
526
- value: query,
527
- onChange: function (e) { setQuery(e.target.value); },
528
- onKeyDown: function (e) { if (e.key === "Enter") doSearch(); },
529
- }),
530
- h("select", {
531
- className: "mkt_select",
532
- value: sort,
533
- onChange: function (e) { setSort(e.target.value); },
534
- },
535
- h("option", { value: "stars" }, "按 star"),
536
- h("option", { value: "updated" }, "最近更新"),
537
- h("option", { value: "forks" }, "按 fork")
538
- ),
539
- h("button", { className: "mkt_btn mkt_btnPrimary", disabled: loading, onClick: doSearch }, loading ? "搜索中…" : "搜索"),
540
- h("button", { className: "mkt_btn", onClick: refreshInstalled }, "刷新已装"),
541
- h("button", { className: "mkt_btn mkt_btnDanger", disabled: restarting, onClick: doRestart }, restarting ? "重启中…" : "重启 dsh"),
542
- h("label", { className: "mkt_check" },
543
- h("input", { type: "checkbox", checked: verifiedOnly, onChange: function (e) { setVerifiedOnly(e.target.checked); } }),
544
- "只看已验证插件")
545
- ),
546
- error ? h("div", { className: "mkt_error" }, error) : null,
547
- h(InstalledPanel, { installed: installed, removing: removing, updates: updates, onUninstall: doUninstall, onInstallSpec: doInstallSpec }),
548
- h(JobsPanel, { jobs: jobs, onClear: clearJobs }),
549
- h("div", { className: "mkt_list" },
550
- results == null
551
- ? h("div", { className: "mkt_meta mkt_listHead" }, loading ? "正在加载最热插件…" : "—")
552
- : results.items.length === 0
553
- ? h("div", { className: "mkt_meta mkt_listHead" }, "没有匹配的仓库。")
554
- : h(React.Fragment, null,
555
- h("div", { className: "mkt_meta mkt_listHead" }, verifiedOnly
556
- ? "已验证插件 " + visibleItems.length + " 个 · 已加载 " + results.items.length + "/" + results.total + " 个仓库(star≥1)" + (verifyPending ? " · 验证中…" : "")
557
- : "共 " + results.total + " 个仓库(star≥1)· 已显示 " + results.items.length + " 个"),
558
- visibleItems.length === 0 && verifiedOnly
559
- ? h("div", { className: "mkt_meta mkt_listHead" }, verifyPending ? "正在验证仓库是否为 dsh 插件…" : "当前加载的结果里没有已验证的 dsh 插件,下滑加载更多。")
560
- : null,
561
- visibleItems.map(function (item) {
562
- var installJob = null;
563
- for (var jobId in jobs) {
564
- if (jobs[jobId] && jobs[jobId].spec === "github:" + item.fullName) { installJob = jobs[jobId]; break; }
565
- }
566
- var vInfo = verified[item.fullName];
567
- var alreadyInstalled = vInfo !== undefined && typeof vInfo.name === "string"
568
- && installed !== null && installed.error === undefined
569
- && (installed.deps || []).some(function (dep) { return dep.name === vInfo.name; });
570
- return h(RepoCard, {
571
- key: item.fullName,
572
- item: item,
573
- installing: installing[item.fullName] === true || installing["github:" + item.fullName] === true,
574
- installJob: installJob,
575
- verified: vInfo,
576
- alreadyInstalled: alreadyInstalled,
577
- onInstall: doInstall,
578
- });
579
- }),
580
- canLoadMore
581
- ? h("div", { className: "mkt_loadMore", ref: sentinelRef }, loadingMore ? "加载中…" : Date.now() < retryAt ? "GitHub 限流中,稍后再下滑加载" : "下滑加载更多")
582
- : h("div", { className: "mkt_loadMore" }, reachedLimit ? "已达 GitHub 搜索上限(前 1000 个结果)" : "已显示全部 " + results.items.length + " 个")
583
- )
584
- )
585
- );
586
- }
587
-
588
- // ── plugin body ─────────────────────────────────────────────────────────
589
- var inject = ["slots", "connection"];
590
- function apply(ctx) {
591
- var rpc = ctx.get("connection").rpc;
592
- ctx.effect(function () {
593
- return ctx.slots.inject("settings.plugins.tab", function () {
594
- return ctx.slots.register({
595
- name: "settings.plugins.tab",
596
- id: "market",
597
- order: 30,
598
- label: "插件市场",
599
- inject: function () {
600
- return { rpc: rpc };
601
- },
602
- }, MarketplaceTab);
603
- });
604
- }, "@1e0zj/dsh-plugin-mall: marketplace tab");
605
- }
606
- exports.apply = apply;
607
- exports.inject = inject;
608
- return module.exports;
609
- },
610
- });
1
+ // Browser half of dsh-plugin-mall: the Settings → Plugins → 插件市场 tab.
2
+ //
3
+ // Client plugin contract (see @deepseek-ai/dsh-client-modules): the bundle
4
+ // registers itself on window.__ModuleLoader__, exports a cordis-style
5
+ // { apply, inject }, and is served at /plugins/@1e0zj/dsh-plugin-mall/client.js.
6
+ // This file is hand-written ES5-ish JS on purpose — no build step — using the
7
+ // React instance the shell shares through the client module loader.
8
+ window.__ModuleLoader__.load({
9
+ id: "@1e0zj/dsh-plugin-mall",
10
+ factory: (require) => {
11
+ var module = { exports: {} };
12
+ var exports = module.exports;
13
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
14
+ var React = require("react");
15
+
16
+ // ── styles ──────────────────────────────────────────────────────────────
17
+ var css = [
18
+ ".mkt_root{display:flex;flex-direction:column;gap:12px;max-width:1040px}",
19
+ ".mkt_head{display:flex;flex-direction:column;gap:2px}",
20
+ ".mkt_title{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary)}",
21
+ ".mkt_sub{font-size:12px;color:var(--dsw-alias-label-tertiary)}",
22
+ ".mkt_row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}",
23
+ ".mkt_input{flex:1;min-width:200px;background:var(--dsw-alias-bg-secondary,#fff);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:6px 10px;font-size:13px;color:var(--dsw-alias-label-primary)}",
24
+ ".mkt_select{background:var(--dsw-alias-bg-secondary,#fff);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:6px 8px;font-size:13px;color:var(--dsw-alias-label-primary)}",
25
+ ".mkt_btn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-secondary,#fff);color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 12px;font-size:13px;cursor:pointer}",
26
+ ".mkt_btn:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}",
27
+ ".mkt_btn:disabled{opacity:.55;cursor:default}",
28
+ ".mkt_btnPrimary{background:var(--dsw-alias-state-business-primary,#2b6cb0);border-color:transparent;color:#fff}",
29
+ ".mkt_btnPrimary:hover:not(:disabled){color:#fff;opacity:.92}",
30
+ ".mkt_btnDanger:hover:not(:disabled){border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}",
31
+ ".mkt_btnSm{padding:3px 10px;font-size:12px}",
32
+ ".mkt_error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}",
33
+ ".mkt_list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;align-items:stretch;min-width:0}",
34
+ "@media (max-width:820px){.mkt_list{grid-template-columns:minmax(0,1fr)}}",
35
+ ".mkt_listHead{grid-column:1/-1;font-size:12px;color:var(--dsw-alias-label-tertiary)}",
36
+ ".mkt_loadMore{grid-column:1/-1;text-align:center;font-size:12px;color:var(--dsw-alias-label-tertiary);padding:10px 0}",
37
+ ".mkt_card{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px 12px;background:var(--dsw-alias-bg-primary,#fff);display:flex;flex-direction:column;gap:6px;min-width:0}",
38
+ ".mkt_cardHead{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}",
39
+ ".mkt_metaRow{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}",
40
+ ".mkt_name{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}",
41
+ ".mkt_meta{font-size:12px;color:var(--dsw-alias-label-tertiary)}",
42
+ ".mkt_desc{font-size:12.5px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
43
+ ".mkt_cardActions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:auto;padding-top:4px}",
44
+ ".mkt_cardActions .mkt_btn{text-decoration:none}",
45
+ ".mkt_panelTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}",
46
+ ".mkt_panelRow{display:flex;align-items:center;gap:8px}",
47
+ ".mkt_panelRow .mkt_link{margin-left:auto}",
48
+ ".mkt_pre{font-family:Consolas,Monaco,monospace;font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-secondary,#f6f7f8);border-radius:6px;padding:8px;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-all}",
49
+ ".mkt_ok{color:var(--dsw-alias-state-success-primary,#2f855a)}",
50
+ ".mkt_badge{display:inline-block;border-radius:999px;padding:1px 8px;font-size:11px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary)}",
51
+ ".mkt_badgeOk{border-color:var(--dsw-alias-state-success-primary,#2f855a);color:var(--dsw-alias-state-success-primary,#2f855a)}",
52
+ ".mkt_badgeBad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}",
53
+ ".mkt_check{display:flex;align-items:center;gap:4px;font-size:12.5px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap}",
54
+ ".mkt_link{color:var(--dsw-alias-state-business-primary);font-size:12px;text-decoration:none;cursor:pointer}",
55
+ ".mkt_installedHead{display:flex;align-items:center;gap:8px;width:100%;background:none;border:0;padding:0;margin:0;cursor:pointer;font:inherit;text-align:left}",
56
+ ".mkt_installedHead:hover .mkt_panelTitle{color:var(--dsw-alias-state-business-primary)}",
57
+ ".mkt_installedToggle{margin-left:auto}",
58
+ ".mkt_depList{display:flex;flex-direction:column;gap:6px;margin-top:8px}",
59
+ ".mkt_depRow{display:flex;justify-content:space-between;align-items:center;gap:8px}",
60
+ ".mkt_depActions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}",
61
+ ".mkt_approve{display:flex;flex-direction:column;gap:8px;border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;padding:10px 12px;margin:4px 0}",
62
+ ".mkt_approveHead{font-size:13px;font-weight:600;color:var(--dsw-alias-state-error-primary)}",
63
+ ".mkt_approvePkg{display:flex;flex-direction:column;gap:4px}",
64
+ ".mkt_approveName{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin-right:8px;overflow-wrap:anywhere}",
65
+ ".mkt_approveCmd{max-height:none;margin:0}",
66
+ ".mkt_jobDone{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
67
+ ].join("\n");
68
+ var tagId = "@1e0zj/dsh-plugin-mall/market-tab.css";
69
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
70
+ var styleTag = document.createElement("style");
71
+ styleTag.dataset.plugin = "@1e0zj/dsh-plugin-mall";
72
+ styleTag.dataset.pluginCss = tagId;
73
+ styleTag.textContent = css;
74
+ document.head.appendChild(styleTag);
75
+ }
76
+
77
+ // ── tiny helpers ────────────────────────────────────────────────────────
78
+ var h = React.createElement;
79
+ var useState = React.useState;
80
+ var useEffect = React.useEffect;
81
+ var useRef = React.useRef;
82
+ var useCallback = React.useCallback;
83
+
84
+ function clip(text, max) {
85
+ var t = String(text == null ? "" : text).replace(/\s+/g, " ").trim();
86
+ return t.length > max ? t.slice(0, max - 1) + "…" : t;
87
+ }
88
+
89
+ function errorText(e) {
90
+ return String(e && e.message ? e.message : e);
91
+ }
92
+
93
+ function kindLabel(kind) {
94
+ if (kind === "bundle") return "宿主插件层";
95
+ if (kind === "client") return "浏览器UI插件";
96
+ if (kind === "missing") return "未解析";
97
+ return "普通依赖";
98
+ }
99
+
100
+ // ── job polling ─────────────────────────────────────────────────────────
101
+ function useJobPolling(call, onSettled) {
102
+ var jobsRef = useRef({});
103
+ var _jobs = useState({});
104
+ var jobs = _jobs[0];
105
+ var setJobs = _jobs[1];
106
+ useEffect(function () {
107
+ var timer = setInterval(function () {
108
+ var current = jobsRef.current;
109
+ var running = Object.keys(current).filter(function (id) {
110
+ var job = current[id];
111
+ return job && job.status !== "completed" && job.status !== "failed" && job.status !== "killed";
112
+ });
113
+ if (running.length === 0) return;
114
+ running.forEach(function (id) {
115
+ call("job", { jobId: id }).then(function (value) {
116
+ var snapshot = value.snapshot || {};
117
+ var old = jobsRef.current[id] || {};
118
+ var wasTerminal = old.status === "completed" || old.status === "failed" || old.status === "killed";
119
+ var nowTerminal = snapshot.status === "completed" || snapshot.status === "failed" || snapshot.status === "killed";
120
+ if (!wasTerminal && nowTerminal && onSettled) {
121
+ try { onSettled(id); } catch (e) { /* best effort */ }
122
+ }
123
+ var output = (old.output || "") + (value.output || "");
124
+ jobsRef.current = Object.assign({}, jobsRef.current, { [id]: Object.assign({}, old, {
125
+ status: snapshot.status,
126
+ detail: snapshot.detail,
127
+ needsApproval: snapshot.needsApproval,
128
+ output: output,
129
+ }) });
130
+ setJobs(Object.assign({}, jobsRef.current));
131
+ }).catch(function () { /* keep polling */ });
132
+ });
133
+ }, 1200);
134
+ return function () { clearInterval(timer); };
135
+ }, [call, onSettled]);
136
+ var track = useCallback(function (id, spec) {
137
+ jobsRef.current = Object.assign({}, jobsRef.current, { [id]: { status: "running", spec: spec, output: "" } });
138
+ setJobs(Object.assign({}, jobsRef.current));
139
+ }, []);
140
+ var clear = useCallback(function () {
141
+ jobsRef.current = {};
142
+ setJobs({});
143
+ }, []);
144
+ var drop = useCallback(function (id) {
145
+ var next = Object.assign({}, jobsRef.current);
146
+ delete next[id];
147
+ jobsRef.current = next;
148
+ setJobs(next);
149
+ }, []);
150
+ return { jobs: jobs, track: track, clear: clear, drop: drop };
151
+ }
152
+
153
+ // ── plugin verification badge ───────────────────────────────────────────
154
+ // verified 来自 /market verify 端点(node 侧拉 raw package.json 判定
155
+ // dsh.bundle/dsh.client 声明,进程内缓存)。unknown 不显示徽章。
156
+ function verifyBadge(verified) {
157
+ if (verified === undefined || verified === null) return null;
158
+ if (verified.hostDeps !== undefined && verified.hostDeps.length > 0) {
159
+ return h("span", { className: "mkt_badge mkt_badgeBad", title: verified.hostDeps.join(", ") }, "宿主依赖风险");
160
+ }
161
+ if (verified.kind === "bundle") return h("span", { className: "mkt_badge mkt_badgeOk" }, "宿主插件");
162
+ if (verified.kind === "client") return h("span", { className: "mkt_badge mkt_badgeOk" }, "UI插件");
163
+ if (verified.kind === "plain") return h("span", { className: "mkt_badge" }, "未声明");
164
+ if (verified.kind === "no-manifest") return h("span", { className: "mkt_badge" }, "无package.json");
165
+ return null;
166
+ }
167
+
168
+ // ── repo card ───────────────────────────────────────────────────────────
169
+ function RepoCard(props) {
170
+ var item = props.item;
171
+ var installing = props.installing === true;
172
+ var job = props.installJob;
173
+ var installLabel = props.alreadyInstalled === true ? "已装" : "安装";
174
+ var installDisabled = installing || props.alreadyInstalled === true;
175
+ if (!installing && job) {
176
+ if (job.status === "running") { installLabel = "安装中…"; installDisabled = true; }
177
+ else if (job.status === "completed") { installLabel = "已装 · 重启生效"; installDisabled = true; }
178
+ else if (job.status === "failed") { installLabel = "安装失败 · 见任务日志"; }
179
+ else { installLabel = "已取消 · 重试"; }
180
+ }
181
+ return h("div", { className: "mkt_card" },
182
+ h("div", { className: "mkt_cardHead" },
183
+ h("span", { className: "mkt_name" }, item.fullName),
184
+ verifyBadge(props.verified),
185
+ item.archived ? h("span", { className: "mkt_badge" }, "archived") : null
186
+ ),
187
+ item.description ? h("div", { className: "mkt_desc" }, item.description) : null,
188
+ h("div", { className: "mkt_metaRow" },
189
+ h("span", { className: "mkt_meta" }, "★" + item.stars),
190
+ item.language ? h("span", { className: "mkt_meta" }, item.language) : null,
191
+ item.license ? h("span", { className: "mkt_meta" }, item.license) : null,
192
+ h("span", { className: "mkt_meta" }, "更新 " + (item.updatedAt || "").slice(0, 10))
193
+ ),
194
+ h("div", { className: "mkt_cardActions" },
195
+ h("button", { className: "mkt_btn mkt_btnPrimary", disabled: installDisabled, onClick: function () { props.onInstall(item.fullName); } },
196
+ installLabel),
197
+ h("a", { className: "mkt_btn", href: item.htmlUrl, target: "_blank", rel: "noreferrer" }, "前往仓库")
198
+ )
199
+ );
200
+ }
201
+
202
+ // ── install-script approval ─────────────────────────────────────────────
203
+ // pnpm 默认拦掉依赖的安装期脚本。放行它们 = 让这些命令以用户的权限在用户
204
+ // 机器上运行,而且在任何插件代码加载之前。这个决定属于用户,所以这里如实
205
+ // 摆出「要批准的到底是什么」——包名、版本、确切的命令、以及它究竟是用户
206
+ // 选的那个插件,还是一个他从没听说过的传递依赖。
207
+ // 措辞刻意不写「安全检查」:批准这些脚本,对插件装好之后会做什么一无所证。
208
+ function ApprovalRequest(props) {
209
+ var pkgs = props.needsApproval || [];
210
+ return h("div", { className: "mkt_approve" },
211
+ h("div", { className: "mkt_approveHead" }, "需要你确认:这次安装会执行安装期代码"),
212
+ pkgs.map(function (p) {
213
+ var facts = [];
214
+ if (typeof p.weeklyDownloads === "number") facts.push("周下载 " + p.weeklyDownloads.toLocaleString());
215
+ facts.push(p.provenance === true ? "有来源证明" : "无来源证明");
216
+ if (typeof p.unpackedSize === "number") facts.push((Math.round(p.unpackedSize / 104857.6) / 10) + " MB");
217
+ return h("div", { key: p.name, className: "mkt_approvePkg" },
218
+ h("div", null,
219
+ h("span", { className: "mkt_approveName" }, p.name + (p.version ? "@" + p.version : "")),
220
+ h("span", { className: "mkt_badge " + (p.direct ? "" : "mkt_badgeBad") },
221
+ p.direct ? "你要装的插件本身" : "传递依赖 · 不是你选的那个包")
222
+ ),
223
+ Object.keys(p.scripts || {}).map(function (k) {
224
+ return h("pre", { key: k, className: "mkt_pre mkt_approveCmd" }, k + ": " + p.scripts[k]);
225
+ }),
226
+ h("div", { className: "mkt_meta" }, facts.join(" · "))
227
+ );
228
+ }),
229
+ h("div", { className: "mkt_meta" }, "这些命令会以你的权限在你的机器上运行,早于任何插件代码加载。"),
230
+ h("div", { className: "mkt_row" },
231
+ h("button", {
232
+ className: "mkt_btn mkt_btnPrimary mkt_btnSm",
233
+ disabled: props.busy === true,
234
+ onClick: function () { props.onApprove(pkgs.map(function (p) { return p.name; })); },
235
+ }, props.busy ? "继续中…" : "允许并继续"),
236
+ h("button", { className: "mkt_btn mkt_btnSm", onClick: props.onDismiss }, "取消")
237
+ )
238
+ );
239
+ }
240
+
241
+ // ── jobs panel ──────────────────────────────────────────────────────────
242
+ function JobsPanel(props) {
243
+ var ids = Object.keys(props.jobs);
244
+ if (ids.length === 0) return null;
245
+ return h("div", { className: "mkt_card" },
246
+ h("div", { className: "mkt_panelRow" },
247
+ h("p", { className: "mkt_panelTitle" }, "任务"),
248
+ props.onClear ? h("span", { className: "mkt_link", onClick: props.onClear }, "清空") : null
249
+ ),
250
+ ids.map(function (id) {
251
+ var job = props.jobs[id];
252
+ var done = job.status === "completed" || job.status === "failed" || job.status === "killed";
253
+ return h("div", { key: id, style: { display: "flex", flexDirection: "column", gap: "4px" } },
254
+ h("div", { className: "mkt_meta" },
255
+ id + " · " + clip(job.spec || "", 40) + " · " + (job.status || "running")),
256
+ // 待批准不是失败:什么都没执行,profile 也没动过。所以不显示
257
+ // 失败文案,直接给出「要批准什么」和两个按钮。
258
+ job.needsApproval && job.needsApproval.length > 0
259
+ ? h(ApprovalRequest, {
260
+ needsApproval: job.needsApproval,
261
+ busy: props.approving === job.spec,
262
+ onApprove: function (names) { props.onDismiss(id); props.onApprove(job.spec, names); },
263
+ onDismiss: function () { props.onDismiss(id); },
264
+ })
265
+ : job.detail ? h("div", { className: "mkt_desc" }, job.detail) : null,
266
+ done && job.status === "completed"
267
+ ? h("div", { className: "mkt_jobDone" },
268
+ h("span", { className: "mkt_ok" }, "完成"),
269
+ h("button", {
270
+ className: "mkt_btn mkt_btnPrimary mkt_btnSm",
271
+ disabled: props.restarting === true,
272
+ onClick: props.onRestart,
273
+ }, props.restarting ? "重启中…" : "重启 dsh 生效"))
274
+ : job.status === "failed" && !(job.needsApproval && job.needsApproval.length > 0)
275
+ ? h("div", { className: "mkt_error" }, "失败,见下方输出")
276
+ : null,
277
+ job.output ? h("pre", { className: "mkt_pre" }, job.output.slice(-4000)) : null
278
+ );
279
+ })
280
+ );
281
+ }
282
+
283
+ // ── installed panel (collapsible summary) ───────────────────────────────
284
+ function InstalledPanel(props) {
285
+ var installed = props.installed;
286
+ var _open = useState(false);
287
+ var open = _open[0];
288
+ var setOpen = _open[1];
289
+ if (!installed) return null;
290
+ var count = installed.error ? 0 : (installed.deps || []).length;
291
+ return h("div", { className: "mkt_card" },
292
+ h("button", {
293
+ type: "button",
294
+ className: "mkt_installedHead",
295
+ onClick: function () { setOpen(!open); },
296
+ "aria-expanded": open ? "true" : "false",
297
+ },
298
+ h("span", { className: "mkt_panelTitle" }, "已装插件"),
299
+ h("span", { className: "mkt_badge" }, count + " 个"),
300
+ h("span", { className: "mkt_meta mkt_installedToggle" }, open ? "收起" : "展开")
301
+ ),
302
+ installed.error
303
+ ? h("div", { className: "mkt_error" }, installed.error)
304
+ : !open ? null
305
+ : (installed.deps || []).length === 0
306
+ ? h("div", { className: "mkt_meta" }, "还没有装过插件")
307
+ : h("div", { className: "mkt_depList" }, (installed.deps || []).map(function (dep) {
308
+ var busy = (props.removing || {})[dep.name] === true;
309
+ var upd = (props.updates || {})[dep.name];
310
+ return h("div", { key: dep.name, className: "mkt_depRow" },
311
+ h("span", { className: "mkt_desc" }, dep.name + "@" + dep.version),
312
+ h("span", { className: "mkt_depActions" },
313
+ h("span", { className: "mkt_badge" }, kindLabel(dep.kind)),
314
+ // 必须带上版本号。传裸包名等于让 pnpm 自己挑"最新可安装
315
+ // 版本",而 pnpm 11 的 minimumReleaseAge 会拒绝发布不足
316
+ // 24 小时的版本并回落到旧版 —— 按钮写着"更新至 0.12.3",
317
+ // pnpm 却报 "Already up to date",点多少次都不动。
318
+ // 带上版本是明确指定,pnpm 照装并自行记一条豁免。
319
+ upd && upd.hasUpdate ? h("button", {
320
+ className: "mkt_btn mkt_btnSm",
321
+ onClick: function () { props.onInstallSpec(dep.name + "@" + upd.latest); },
322
+ }, "更新至 " + upd.latest) : null,
323
+ h("button", {
324
+ className: "mkt_btn mkt_btnDanger mkt_btnSm",
325
+ disabled: busy,
326
+ onClick: function () { props.onUninstall(dep.name); },
327
+ }, busy ? "卸载中…" : "卸载")));
328
+ }))
329
+ );
330
+ }
331
+
332
+ // ── the marketplace tab ─────────────────────────────────────────────────
333
+ function MarketplaceTab(props) {
334
+ var rpc = props.rpc;
335
+ var _query = useState("");
336
+ var query = _query[0];
337
+ var setQuery = _query[1];
338
+ var _sort = useState("stars");
339
+ var sort = _sort[0];
340
+ var setSort = _sort[1];
341
+ var _results = useState(null);
342
+ var results = _results[0];
343
+ var setResults = _results[1];
344
+ var _loading = useState(false);
345
+ var loading = _loading[0];
346
+ var setLoading = _loading[1];
347
+ var _error = useState(null);
348
+ var error = _error[0];
349
+ var setError = _error[1];
350
+ var _installed = useState(null);
351
+ var installed = _installed[0];
352
+ var setInstalled = _installed[1];
353
+ var _installing = useState({});
354
+ var installing = _installing[0];
355
+ var setInstalling = _installing[1];
356
+ var _removing = useState({});
357
+ var removing = _removing[0];
358
+ var setRemoving = _removing[1];
359
+ var _page = useState(1);
360
+ var page = _page[0];
361
+ var setPage = _page[1];
362
+ var _loadingMore = useState(false);
363
+ var loadingMore = _loadingMore[0];
364
+ var setLoadingMore = _loadingMore[1];
365
+ var sentinelRef = useRef(null);
366
+ var _verified = useState({});
367
+ var verified = _verified[0];
368
+ var setVerified = _verified[1];
369
+ var _verifiedOnly = useState(true);
370
+ var verifiedOnly = _verifiedOnly[0];
371
+ var setVerifiedOnly = _verifiedOnly[1];
372
+ var _updates = useState(null);
373
+ var updates = _updates[0];
374
+ var setUpdates = _updates[1];
375
+ var _reachedLimit = useState(false);
376
+ var reachedLimit = _reachedLimit[0];
377
+ var setReachedLimit = _reachedLimit[1];
378
+ var _retryAt = useState(0);
379
+ var retryAt = _retryAt[0];
380
+ var setRetryAt = _retryAt[1];
381
+ var _restarting = useState(false);
382
+ var restarting = _restarting[0];
383
+ var setRestarting = _restarting[1];
384
+
385
+ var call = useCallback(function (endpoint, payload) {
386
+ return rpc.call("/market", endpoint, payload || {}).then(function (res) {
387
+ if (!res || res.ok !== true) {
388
+ var err = res && res.error;
389
+ var msg = err && typeof err === "object" ? (err.message || JSON.stringify(err)) : (err || "market request failed");
390
+ throw new Error(msg);
391
+ }
392
+ return res.value;
393
+ });
394
+ }, [rpc]);
395
+
396
+ // 每页搜索结果到货后批量验证(node raw CDN,带缓存),失败静默——
397
+ // 徽章缺失可接受,不该打断浏览。
398
+ var verifyPage = useCallback(function (items) {
399
+ var repos = (items || []).map(function (it) { return it.fullName; });
400
+ if (repos.length === 0) return;
401
+ call("verify", { repos: repos }).then(function (value) {
402
+ setVerified(function (prev) { return Object.assign({}, prev, value.results); });
403
+ }).catch(function () { /* 徽章缺失即可 */ });
404
+ }, [call]);
405
+
406
+ var doSearch = useCallback(function () {
407
+ setLoading(true);
408
+ setError(null);
409
+ setPage(1);
410
+ setReachedLimit(false);
411
+ setRetryAt(0);
412
+ call("search", { query: query, sort: sort, perPage: 20 }).then(function (value) {
413
+ setResults(value);
414
+ setVerified({});
415
+ verifyPage(value.items);
416
+ }).catch(function (e) {
417
+ setError(errorText(e));
418
+ }).finally(function () {
419
+ setLoading(false);
420
+ });
421
+ }, [call, query, sort, verifyPage]);
422
+
423
+ // 无限滚动:哨兵进入视口时拉下一页并追加。GitHub topic 的翻页间数据
424
+ // 可能移动造成重复,按 fullName 去重;已显示数追上 total 即到底。
425
+ var canLoadMore = results !== null && results.items.length < results.total && !reachedLimit;
426
+ var loadMoreLock = useRef(false);
427
+ var loadMore = useCallback(function () {
428
+ if (!canLoadMore || loading || loadingMore) return;
429
+ // 同一 tick observer 可能连续触发两次,state 更新不阻塞闭包读值,
430
+ // ref 做重入闸。
431
+ if (loadMoreLock.current) return;
432
+ // 限流熔断:失败后 60s 内不再自动请求(哨兵重渲染会反复触发
433
+ // IntersectionObserver,不熔断会连环 500 直到限流窗口过去)。
434
+ if (Date.now() < retryAt) return;
435
+ loadMoreLock.current = true;
436
+ setLoadingMore(true);
437
+ call("search", { query: query, sort: sort, perPage: 20, page: page + 1 }).then(function (value) {
438
+ if (value.truncated === true) { setReachedLimit(true); return; }
439
+ setPage(page + 1);
440
+ verifyPage(value.items);
441
+ setResults(function (prev) {
442
+ if (prev === null) return value;
443
+ var seen = {};
444
+ var merged = [];
445
+ prev.items.concat(value.items).forEach(function (it) {
446
+ if (seen[it.fullName] === true) return;
447
+ seen[it.fullName] = true;
448
+ merged.push(it);
449
+ });
450
+ return { total: value.total, items: merged };
451
+ });
452
+ }).catch(function (e) {
453
+ setError(errorText(e));
454
+ setRetryAt(Date.now() + 60000);
455
+ }).finally(function () {
456
+ loadMoreLock.current = false;
457
+ setLoadingMore(false);
458
+ });
459
+ }, [call, canLoadMore, loading, loadingMore, page, query, sort, retryAt]);
460
+
461
+ useEffect(function () {
462
+ var node = sentinelRef.current;
463
+ if (node === null || node === undefined) return undefined;
464
+ if (typeof IntersectionObserver === "undefined") return undefined;
465
+ var observer = new IntersectionObserver(function (entries) {
466
+ for (var index = 0; index < entries.length; index++) {
467
+ if (entries[index].isIntersecting) { loadMore(); break; }
468
+ }
469
+ }, { rootMargin: "300px" });
470
+ observer.observe(node);
471
+ return function () { observer.disconnect(); };
472
+ }, [loadMore]);
473
+
474
+ var refreshInstalled = useCallback(function () {
475
+ call("installed", {}).then(function (value) {
476
+ setInstalled(value);
477
+ call("updates", {}).then(setUpdates).catch(function () { setUpdates(null); });
478
+ }).catch(function (e) {
479
+ setInstalled({ error: errorText(e) });
480
+ });
481
+ }, [call]);
482
+
483
+ // 点开即自动搜索(空关键词 = 最热插件),安装任务结束自动刷新已装列表。
484
+ var polling = useJobPolling(call, refreshInstalled);
485
+ var track = polling.track;
486
+ var jobs = polling.jobs;
487
+ var clearJobs = polling.clear;
488
+ var dropJob = polling.drop;
489
+
490
+ useEffect(function () {
491
+ doSearch();
492
+ // eslint-disable-next-line react-hooks/exhaustive-deps
493
+ }, []);
494
+
495
+ // 通用安装入口:spec 可以是 github:owner/repo(卡片按钮)或 npm 包名
496
+ // (已装面板的更新按钮)。后端会把同源发布的 github spec 改写为 npm
497
+ // tarball 安装(更快),job 里记录的是最终 spec。
498
+ var doInstallSpec = useCallback(function (spec, allowBuildScripts) {
499
+ setInstalling(function (prev) {
500
+ var next = Object.assign({}, prev, { [spec]: true });
501
+ return next;
502
+ });
503
+ setError(null);
504
+ var payload = { spec: spec };
505
+ if (allowBuildScripts) payload.allowBuildScripts = allowBuildScripts;
506
+ call("install", payload).then(function (value) {
507
+ track(value.jobId, spec);
508
+ }).catch(function (e) {
509
+ setError(errorText(e));
510
+ }).finally(function () {
511
+ setInstalling(function (prev) {
512
+ var next = Object.assign({}, prev);
513
+ delete next[spec];
514
+ return next;
515
+ });
516
+ });
517
+ }, [call, track]);
518
+
519
+ // 批准 = 用同一个 spec 带着点名的同意重新发起一次安装。不需要可恢复的
520
+ // 任务状态机:pnpm store 已经缓存了刚下载的东西,第二次几乎不花时间。
521
+ var doApprove = useCallback(function (spec, names) {
522
+ doInstallSpec(spec, names);
523
+ }, [doInstallSpec]);
524
+
525
+ var doInstall = useCallback(function (repo) {
526
+ doInstallSpec("github:" + repo);
527
+ }, [doInstallSpec]);
528
+
529
+ // 一键重启:loopback-only 的 /market restart 端点会 detached 拉起新的
530
+ // dsh 进程后退出当前进程;这里轮询 host 恢复后自动刷新页面。
531
+ var doRestart = useCallback(function () {
532
+ if (typeof window !== "undefined" && typeof window.confirm === "function") {
533
+ if (window.confirm("重启 dsh?正在进行的任务会中断。") !== true) return;
534
+ }
535
+ setRestarting(true);
536
+ setError(null);
537
+ call("restart", {}).then(function () {
538
+ var tries = 0;
539
+ var ping = setInterval(function () {
540
+ tries++;
541
+ if (tries > 40) {
542
+ clearInterval(ping);
543
+ setRestarting(false);
544
+ setError("2 分钟内未检测到 dsh 重启完成,请手动检查 dsh 状态后刷新页面。");
545
+ return;
546
+ }
547
+ rpc.call("/market", "installed", {}).then(function () {
548
+ clearInterval(ping);
549
+ window.location.reload();
550
+ }).catch(function () { /* host 还在重启,继续等 */ });
551
+ }, 3000);
552
+ }).catch(function (e) {
553
+ setRestarting(false);
554
+ setError(errorText(e));
555
+ });
556
+ }, [call, rpc]);
557
+
558
+ var doUninstall = useCallback(function (name) {
559
+ setRemoving(function (prev) {
560
+ var next = Object.assign({}, prev, { [name]: true });
561
+ return next;
562
+ });
563
+ setError(null);
564
+ call("uninstall", { package: name }).then(function (value) {
565
+ track(value.jobId, name);
566
+ }).catch(function (e) {
567
+ setError(errorText(e));
568
+ }).finally(function () {
569
+ setRemoving(function (prev) {
570
+ var next = Object.assign({}, prev);
571
+ delete next[name];
572
+ return next;
573
+ });
574
+ });
575
+ }, [call, track]);
576
+
577
+ useEffect(function () {
578
+ refreshInstalled();
579
+ }, [refreshInstalled]);
580
+
581
+ // 任务面板位置固定(已装面板之后):不随任务状态在顶部/底部之间
582
+ // 跳——npm 安装几秒即完成,"完成即落底"曾让结果凭空消失。
583
+ // "只看已验证"开关下的可见项:verify 判定 bundle/client 的才算插件。
584
+ // verifyPending:当前加载的仓库里还有未验证完的(verified map 尚未覆盖)。
585
+ var visibleItems = results === null ? [] : results.items.filter(function (it) {
586
+ if (verifiedOnly !== true) return true;
587
+ var v = verified[it.fullName];
588
+ return v !== undefined && (v.kind === "bundle" || v.kind === "client")
589
+ && !(v.hostDeps !== undefined && v.hostDeps.length > 0);
590
+ });
591
+ var verifyPending = results !== null && results.items.some(function (it) { return verified[it.fullName] === undefined; });
592
+
593
+ return h("div", { className: "mkt_root" },
594
+ h("div", { className: "mkt_head" },
595
+ h("div", { className: "mkt_title" }, "插件市场"),
596
+ h("div", { className: "mkt_sub" }, "搜索 GitHub dsh-plugin 话题插件 · 装/卸后需重启 dsh 生效")
597
+ ),
598
+ h("div", { className: "mkt_row" },
599
+ h("input", {
600
+ className: "mkt_input",
601
+ placeholder: "搜索插件(留空列出最热)",
602
+ value: query,
603
+ onChange: function (e) { setQuery(e.target.value); },
604
+ onKeyDown: function (e) { if (e.key === "Enter") doSearch(); },
605
+ }),
606
+ h("select", {
607
+ className: "mkt_select",
608
+ value: sort,
609
+ onChange: function (e) { setSort(e.target.value); },
610
+ },
611
+ h("option", { value: "stars" }, "按 star"),
612
+ h("option", { value: "updated" }, "最近更新"),
613
+ h("option", { value: "forks" }, "按 fork")
614
+ ),
615
+ h("button", { className: "mkt_btn mkt_btnPrimary", disabled: loading, onClick: doSearch }, loading ? "搜索中…" : "搜索"),
616
+ h("button", { className: "mkt_btn", onClick: refreshInstalled }, "刷新已装"),
617
+ h("button", { className: "mkt_btn mkt_btnDanger", disabled: restarting, onClick: doRestart }, restarting ? "重启中…" : "重启 dsh"),
618
+ h("label", { className: "mkt_check" },
619
+ h("input", { type: "checkbox", checked: verifiedOnly, onChange: function (e) { setVerifiedOnly(e.target.checked); } }),
620
+ "只看已验证插件")
621
+ ),
622
+ error ? h("div", { className: "mkt_error" }, error) : null,
623
+ h(InstalledPanel, { installed: installed, removing: removing, updates: updates, onUninstall: doUninstall, onInstallSpec: doInstallSpec }),
624
+ h(JobsPanel, {
625
+ jobs: jobs,
626
+ onClear: clearJobs,
627
+ onApprove: doApprove,
628
+ onDismiss: dropJob,
629
+ onRestart: doRestart,
630
+ restarting: restarting,
631
+ approving: Object.keys(installing).filter(function (s) { return installing[s]; })[0],
632
+ }),
633
+ h("div", { className: "mkt_list" },
634
+ results == null
635
+ ? h("div", { className: "mkt_meta mkt_listHead" }, loading ? "正在加载最热插件…" : "—")
636
+ : results.items.length === 0
637
+ ? h("div", { className: "mkt_meta mkt_listHead" }, "没有匹配的仓库。")
638
+ : h(React.Fragment, null,
639
+ h("div", { className: "mkt_meta mkt_listHead" }, verifiedOnly
640
+ ? "已验证插件 " + visibleItems.length + " 个 · 已加载 " + results.items.length + "/" + results.total + " 个仓库(star≥1)" + (verifyPending ? " · 验证中…" : "")
641
+ : "共 " + results.total + " 个仓库(star≥1)· 已显示 " + results.items.length + " 个"),
642
+ visibleItems.length === 0 && verifiedOnly
643
+ ? h("div", { className: "mkt_meta mkt_listHead" }, verifyPending ? "正在验证仓库是否为 dsh 插件…" : "当前加载的结果里没有已验证的 dsh 插件,下滑加载更多。")
644
+ : null,
645
+ visibleItems.map(function (item) {
646
+ var installJob = null;
647
+ for (var jobId in jobs) {
648
+ if (jobs[jobId] && jobs[jobId].spec === "github:" + item.fullName) { installJob = jobs[jobId]; break; }
649
+ }
650
+ var vInfo = verified[item.fullName];
651
+ var alreadyInstalled = vInfo !== undefined && typeof vInfo.name === "string"
652
+ && installed !== null && installed.error === undefined
653
+ && (installed.deps || []).some(function (dep) { return dep.name === vInfo.name; });
654
+ return h(RepoCard, {
655
+ key: item.fullName,
656
+ item: item,
657
+ installing: installing[item.fullName] === true || installing["github:" + item.fullName] === true,
658
+ installJob: installJob,
659
+ verified: vInfo,
660
+ alreadyInstalled: alreadyInstalled,
661
+ onInstall: doInstall,
662
+ });
663
+ }),
664
+ canLoadMore
665
+ ? h("div", { className: "mkt_loadMore", ref: sentinelRef }, loadingMore ? "加载中…" : Date.now() < retryAt ? "GitHub 限流中,稍后再下滑加载" : "下滑加载更多")
666
+ : h("div", { className: "mkt_loadMore" }, reachedLimit ? "已达 GitHub 搜索上限(前 1000 个结果)" : "已显示全部 " + results.items.length + " 个")
667
+ )
668
+ )
669
+ );
670
+ }
671
+
672
+ // ── plugin body ─────────────────────────────────────────────────────────
673
+ var inject = ["slots", "connection"];
674
+ function apply(ctx) {
675
+ var rpc = ctx.get("connection").rpc;
676
+ ctx.effect(function () {
677
+ return ctx.slots.inject("settings.plugins.tab", function () {
678
+ return ctx.slots.register({
679
+ name: "settings.plugins.tab",
680
+ id: "market",
681
+ order: 30,
682
+ label: "插件市场",
683
+ inject: function () {
684
+ return { rpc: rpc };
685
+ },
686
+ }, MarketplaceTab);
687
+ });
688
+ }, "@1e0zj/dsh-plugin-mall: marketplace tab");
689
+ }
690
+ exports.apply = apply;
691
+ exports.inject = inject;
692
+ return module.exports;
693
+ },
694
+ });