dsh-plugin-marketplace 0.2.2 → 0.2.3
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.md +78 -53
- package/README.zh.md +64 -44
- package/client.js +402 -357
- package/cordis.patch.yml +10 -0
- package/index.js +253 -162
- package/package.json +7 -2
- package/vendor/dsh-settings-expose.js +38 -7
package/client.js
CHANGED
|
@@ -1,357 +1,402 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-plugin-marketplace — browser half.
|
|
3
|
-
*
|
|
4
|
-
* A marketplace section inside the Web UI settings page: browses
|
|
5
|
-
* github.com/topics/dsh-plugin through the public GitHub search API
|
|
6
|
-
* (CORS-enabled), with keyword search, star/update sorting, paging, and a
|
|
7
|
-
* per-plugin detail panel (README summary + install instructions).
|
|
8
|
-
*
|
|
9
|
-
* Hand-written ModuleLoader bundle — no build step required.
|
|
10
|
-
*/
|
|
11
|
-
window.__ModuleLoader__.load({
|
|
12
|
-
id: "dsh-plugin-marketplace",
|
|
13
|
-
factory: (require) => {
|
|
14
|
-
var module = { exports: {} };
|
|
15
|
-
var exports = module.exports;
|
|
16
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
17
|
-
var react = require("react");
|
|
18
|
-
var h = react.createElement;
|
|
19
|
-
|
|
20
|
-
// ── CSS (theme tokens) ────────────────────────────────────────────────
|
|
21
|
-
var CSS = ".__mp_grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px;padding:0;margin:0;list-style:none}" +
|
|
22
|
-
".__mp_item{display:flex;flex-direction:column;gap:8px}" +
|
|
23
|
-
".__mp_card{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:6px;background:var(--dsw-alias-bg-layer-2);cursor:pointer;text-align:left;font:inherit;color:inherit}" +
|
|
24
|
-
".__mp_card:hover{border-color:var(--dsw-alias-brand-primary)}" +
|
|
25
|
-
".__mp_cardHead{display:flex;align-items:center;gap:8px;min-width:0}" +
|
|
26
|
-
".__mp_name{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}" +
|
|
27
|
-
".__mp_star{flex:none;font-size:12px;color:var(--dsw-alias-label-secondary)}" +
|
|
28
|
-
".__mp_desc{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}" +
|
|
29
|
-
".__mp_meta{display:flex;gap:10px;font-size:11px;color:var(--dsw-alias-label-tertiary)}" +
|
|
30
|
-
".__mp_toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px}" +
|
|
31
|
-
".__mp_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;min-width:200px;flex:1}" +
|
|
32
|
-
".__mp_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 8px;font-size:13px}" +
|
|
33
|
-
".__mp_more{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 14px;font:inherit;font-size:13px;cursor:pointer;margin-top:12px}" +
|
|
34
|
-
".__mp_more:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary)}" +
|
|
35
|
-
".__mp_more:disabled{opacity:.5;cursor:default}" +
|
|
36
|
-
".__mp_status{font-size:12px;color:var(--dsw-alias-label-tertiary);margin:8px 0 0}" +
|
|
37
|
-
".__mp_detail{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:14px;margin-top:10px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px;width:100%;box-sizing:border-box}" +
|
|
38
|
-
".__mp_detailTitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}" +
|
|
39
|
-
".__mp_readme{font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word;max-height:260px;overflow:auto;width:100%;box-sizing:border-box;min-width:0}" +
|
|
40
|
-
".__mp_code{display:block;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:6px;padding:8px 10px;font-family:ui-monospace,Consolas,monospace;font-size:12px;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-all;width:100%;box-sizing:border-box;min-width:0}" +
|
|
41
|
-
".__mp_link{color:var(--dsw-alias-brand-primary);font-size:12px;text-decoration:none}" +
|
|
42
|
-
".__mp_btnPrimary{border-color:var(--dsw-alias-state-business-primary, #679efe);background:var(--dsw-alias-state-business-primary, #679efe);color:#fff}" +
|
|
43
|
-
".__mp_error{color:var(--dsw-alias-label-error);font-size:12px;margin:8px 0 0}";
|
|
44
|
-
var tagId = "dsh-plugin-marketplace/main.css";
|
|
45
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
46
|
-
var tag = document.createElement("style");
|
|
47
|
-
tag.dataset.plugin = "dsh-plugin-marketplace";
|
|
48
|
-
tag.dataset.pluginCss = tagId;
|
|
49
|
-
tag.textContent = CSS;
|
|
50
|
-
document.head.appendChild(tag);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ── locale ────────────────────────────────────────────────────────────
|
|
54
|
-
var NS = "marketplace";
|
|
55
|
-
var inject = ["slots", "locale", "settingsScope", "connection"];
|
|
56
|
-
var zh = {
|
|
57
|
-
nav: "插件市场",
|
|
58
|
-
search: "搜索插件(关键词或留空浏览全部)…",
|
|
59
|
-
sortStars: "按 Star 排序",
|
|
60
|
-
sortUpdated: "按更新时间排序",
|
|
61
|
-
loadMore: "加载更多",
|
|
62
|
-
loading: "加载中…",
|
|
63
|
-
error: "加载失败,请稍后重试",
|
|
64
|
-
empty: "没有找到插件",
|
|
65
|
-
install: "安装方式",
|
|
66
|
-
installHint: "在 profile 目录执行 pnpm 安装,然后在 cordis.patch.yml 挂载:",
|
|
67
|
-
readme: "README 摘要",
|
|
68
|
-
readmeEmpty: "(该仓库没有 README)",
|
|
69
|
-
openRepo: "打开 GitHub 仓库 ↗",
|
|
70
|
-
openNpm: "在 npm 搜索同名包 ↗",
|
|
71
|
-
updated: "更新",
|
|
72
|
-
total: "共 {count} 个插件",
|
|
73
|
-
ghError: "GitHub API 请求失败(未认证限流 60 次/小时),请稍后再试",
|
|
74
|
-
installBtn: "一键安装",
|
|
75
|
-
confirmInstall: "确认安装 {pkg}?",
|
|
76
|
-
installing: "安装中…",
|
|
77
|
-
installOk: "已安装:{msg}",
|
|
78
|
-
installErr: "安装失败:{msg}",
|
|
79
|
-
installIdle: "",
|
|
80
|
-
installingHint: "正在后台执行 dsh plugin add,请稍候…"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
var
|
|
154
|
-
var
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
var
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}, props.t("
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
"
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
h("div", { className: "__mp_detailTitle" }, props.t("
|
|
227
|
-
h("div", { className: "__mp_readme" },
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
});
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
),
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-marketplace — browser half.
|
|
3
|
+
*
|
|
4
|
+
* A marketplace section inside the Web UI settings page: browses
|
|
5
|
+
* github.com/topics/dsh-plugin through the public GitHub search API
|
|
6
|
+
* (CORS-enabled), with keyword search, star/update sorting, paging, and a
|
|
7
|
+
* per-plugin detail panel (README summary + install instructions).
|
|
8
|
+
*
|
|
9
|
+
* Hand-written ModuleLoader bundle — no build step required.
|
|
10
|
+
*/
|
|
11
|
+
window.__ModuleLoader__.load({
|
|
12
|
+
id: "dsh-plugin-marketplace",
|
|
13
|
+
factory: (require) => {
|
|
14
|
+
var module = { exports: {} };
|
|
15
|
+
var exports = module.exports;
|
|
16
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
17
|
+
var react = require("react");
|
|
18
|
+
var h = react.createElement;
|
|
19
|
+
|
|
20
|
+
// ── CSS (theme tokens) ────────────────────────────────────────────────
|
|
21
|
+
var CSS = ".__mp_grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px;padding:0;margin:0;list-style:none}" +
|
|
22
|
+
".__mp_item{display:flex;flex-direction:column;gap:8px}" +
|
|
23
|
+
".__mp_card{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:6px;background:var(--dsw-alias-bg-layer-2);cursor:pointer;text-align:left;font:inherit;color:inherit}" +
|
|
24
|
+
".__mp_card:hover{border-color:var(--dsw-alias-brand-primary)}" +
|
|
25
|
+
".__mp_cardHead{display:flex;align-items:center;gap:8px;min-width:0}" +
|
|
26
|
+
".__mp_name{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}" +
|
|
27
|
+
".__mp_star{flex:none;font-size:12px;color:var(--dsw-alias-label-secondary)}" +
|
|
28
|
+
".__mp_desc{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}" +
|
|
29
|
+
".__mp_meta{display:flex;gap:10px;font-size:11px;color:var(--dsw-alias-label-tertiary)}" +
|
|
30
|
+
".__mp_toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px}" +
|
|
31
|
+
".__mp_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;min-width:200px;flex:1}" +
|
|
32
|
+
".__mp_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 8px;font-size:13px}" +
|
|
33
|
+
".__mp_more{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 14px;font:inherit;font-size:13px;cursor:pointer;margin-top:12px}" +
|
|
34
|
+
".__mp_more:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary)}" +
|
|
35
|
+
".__mp_more:disabled{opacity:.5;cursor:default}" +
|
|
36
|
+
".__mp_status{font-size:12px;color:var(--dsw-alias-label-tertiary);margin:8px 0 0}" +
|
|
37
|
+
".__mp_detail{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:14px;margin-top:10px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px;width:100%;box-sizing:border-box}" +
|
|
38
|
+
".__mp_detailTitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}" +
|
|
39
|
+
".__mp_readme{font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word;max-height:260px;overflow:auto;width:100%;box-sizing:border-box;min-width:0}" +
|
|
40
|
+
".__mp_code{display:block;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:6px;padding:8px 10px;font-family:ui-monospace,Consolas,monospace;font-size:12px;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-all;width:100%;box-sizing:border-box;min-width:0}" +
|
|
41
|
+
".__mp_link{color:var(--dsw-alias-brand-primary);font-size:12px;text-decoration:none}" +
|
|
42
|
+
".__mp_btnPrimary{border-color:var(--dsw-alias-state-business-primary, #679efe);background:var(--dsw-alias-state-business-primary, #679efe);color:#fff}" +
|
|
43
|
+
".__mp_error{color:var(--dsw-alias-label-error);font-size:12px;margin:8px 0 0}";
|
|
44
|
+
var tagId = "dsh-plugin-marketplace/main.css";
|
|
45
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
46
|
+
var tag = document.createElement("style");
|
|
47
|
+
tag.dataset.plugin = "dsh-plugin-marketplace";
|
|
48
|
+
tag.dataset.pluginCss = tagId;
|
|
49
|
+
tag.textContent = CSS;
|
|
50
|
+
document.head.appendChild(tag);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── locale ────────────────────────────────────────────────────────────
|
|
54
|
+
var NS = "marketplace";
|
|
55
|
+
var inject = ["slots", "locale", "settingsScope", "connection"];
|
|
56
|
+
var zh = {
|
|
57
|
+
nav: "插件市场",
|
|
58
|
+
search: "搜索插件(关键词或留空浏览全部)…",
|
|
59
|
+
sortStars: "按 Star 排序",
|
|
60
|
+
sortUpdated: "按更新时间排序",
|
|
61
|
+
loadMore: "加载更多",
|
|
62
|
+
loading: "加载中…",
|
|
63
|
+
error: "加载失败,请稍后重试",
|
|
64
|
+
empty: "没有找到插件",
|
|
65
|
+
install: "安装方式",
|
|
66
|
+
installHint: "在 profile 目录执行 pnpm 安装,然后在 cordis.patch.yml 挂载:",
|
|
67
|
+
readme: "README 摘要",
|
|
68
|
+
readmeEmpty: "(该仓库没有 README)",
|
|
69
|
+
openRepo: "打开 GitHub 仓库 ↗",
|
|
70
|
+
openNpm: "在 npm 搜索同名包 ↗",
|
|
71
|
+
updated: "更新",
|
|
72
|
+
total: "共 {count} 个插件",
|
|
73
|
+
ghError: "GitHub API 请求失败(未认证限流 60 次/小时),请稍后再试",
|
|
74
|
+
installBtn: "一键安装",
|
|
75
|
+
confirmInstall: "确认安装 {pkg}?",
|
|
76
|
+
installing: "安装中…",
|
|
77
|
+
installOk: "已安装:{msg}",
|
|
78
|
+
installErr: "安装失败:{msg}",
|
|
79
|
+
installIdle: "",
|
|
80
|
+
installingHint: "正在后台执行 dsh plugin add,请稍候…",
|
|
81
|
+
aiExplain: "🤖 AI 解释",
|
|
82
|
+
aiExplaining: "AI 解释中,请稍候…",
|
|
83
|
+
aiExplainErr: "AI 解释失败:{msg}",
|
|
84
|
+
notExposed: "设置通道未就绪:插件市场的命名空间还没被配置客户端放行。刚安装/升级过的话,请重启 dsh web;若重启后仍报错,请查看启动日志里的 [settings-expose] 提示(可能需要手动把 \"plugin-marketplace\" 加入 dsh-host-apiproxy 的 WEB_SETTINGS_NAMESPACES)。"
|
|
85
|
+
};
|
|
86
|
+
var en = {
|
|
87
|
+
nav: "Plugin Marketplace",
|
|
88
|
+
search: "Search plugins (keyword, or empty to browse all)…",
|
|
89
|
+
sortStars: "Sort by stars",
|
|
90
|
+
sortUpdated: "Sort by updated",
|
|
91
|
+
loadMore: "Load more",
|
|
92
|
+
loading: "Loading…",
|
|
93
|
+
error: "Failed to load, try again later",
|
|
94
|
+
empty: "No plugins found",
|
|
95
|
+
install: "Install",
|
|
96
|
+
installHint: "Run pnpm in your profile dir, then mount in cordis.patch.yml:",
|
|
97
|
+
readme: "README summary",
|
|
98
|
+
readmeEmpty: "(no README in this repo)",
|
|
99
|
+
openRepo: "Open GitHub repo ↗",
|
|
100
|
+
openNpm: "Search npm ↗",
|
|
101
|
+
updated: "Updated",
|
|
102
|
+
total: "{count} plugins",
|
|
103
|
+
ghError: "GitHub API rate-limited (60/hr unauthenticated), try again later",
|
|
104
|
+
installBtn: "Install",
|
|
105
|
+
confirmInstall: "Install {pkg}?",
|
|
106
|
+
installing: "Installing…",
|
|
107
|
+
installOk: "Installed: {msg}",
|
|
108
|
+
installErr: "Install failed: {msg}",
|
|
109
|
+
installIdle: "",
|
|
110
|
+
installingHint: "Running dsh plugin add in the background…",
|
|
111
|
+
aiExplain: "🤖 AI Explain",
|
|
112
|
+
aiExplaining: "AI is explaining…",
|
|
113
|
+
aiExplainErr: "AI explain failed: {msg}",
|
|
114
|
+
notExposed: "Settings channel not ready: the plugin-marketplace namespace is not yet exposed to configuration clients. If you just installed/upgraded, restart dsh web; if it persists, check the [settings-expose] lines in the boot log (you may need to add \"plugin-marketplace\" to WEB_SETTINGS_NAMESPACES in dsh-host-apiproxy/lib/index.js manually)."
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// ── GitHub API ────────────────────────────────────────────────────────
|
|
118
|
+
var BASE = "https://api.github.com/search/repositories";
|
|
119
|
+
var cache = new Map(); // queryKey -> {items, incomplete}
|
|
120
|
+
function queryKey(q, sort, page) { return q + "|" + sort + "|" + page; }
|
|
121
|
+
function fmtTime(iso) {
|
|
122
|
+
try { return new Date(iso).toISOString().slice(0, 10); } catch { return ""; }
|
|
123
|
+
}
|
|
124
|
+
async function fetchPage(q, sort, page) {
|
|
125
|
+
var key = queryKey(q, sort, page);
|
|
126
|
+
var hit = cache.get(key);
|
|
127
|
+
if (hit) return hit;
|
|
128
|
+
var params = new URLSearchParams({
|
|
129
|
+
q: "topic:dsh-plugin" + (q ? " " + q : ""),
|
|
130
|
+
sort: sort,
|
|
131
|
+
order: "desc",
|
|
132
|
+
per_page: "20",
|
|
133
|
+
page: String(page)
|
|
134
|
+
});
|
|
135
|
+
var res = await fetch(BASE + "?" + params.toString(), { headers: { Accept: "application/vnd.github+json" } });
|
|
136
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
137
|
+
var data = await res.json();
|
|
138
|
+
var items = (data.items || []).map(function (r) {
|
|
139
|
+
return {
|
|
140
|
+
fullName: r.full_name,
|
|
141
|
+
desc: r.description || "",
|
|
142
|
+
stars: r.stargazers_count || 0,
|
|
143
|
+
updated: r.updated_at || "",
|
|
144
|
+
lang: r.language || "",
|
|
145
|
+
htmlUrl: r.html_url
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
var out = { items: items, total: data.total_count || 0 };
|
|
149
|
+
cache.set(key, out);
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
async function fetchReadme(fullName) {
|
|
153
|
+
var url = "https://api.github.com/repos/" + fullName + "/readme";
|
|
154
|
+
var res = await fetch(url, { headers: { Accept: "application/vnd.github+json" } });
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
var e = new Error("readme http " + res.status);
|
|
157
|
+
e.status = res.status;
|
|
158
|
+
throw e;
|
|
159
|
+
}
|
|
160
|
+
var data = await res.json();
|
|
161
|
+
var bin = atob(data.content.replace(/\s+/g, ""));
|
|
162
|
+
var bytes = new Uint8Array(bin.length);
|
|
163
|
+
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
164
|
+
return new TextDecoder("utf-8").decode(bytes);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── components ────────────────────────────────────────────────────────
|
|
168
|
+
function PluginCard(props) {
|
|
169
|
+
return h("li", null,
|
|
170
|
+
h("button", { type: "button", className: "__mp_card", onClick: props.onOpen },
|
|
171
|
+
h("div", { className: "__mp_cardHead" },
|
|
172
|
+
h("span", { className: "__mp_name", title: props.plugin.fullName }, props.plugin.fullName),
|
|
173
|
+
h("span", { className: "__mp_star" }, "★ " + props.plugin.stars)
|
|
174
|
+
),
|
|
175
|
+
props.plugin.desc ? h("div", { className: "__mp_desc" }, props.plugin.desc) : null,
|
|
176
|
+
h("div", { className: "__mp_meta" },
|
|
177
|
+
props.plugin.lang ? h("span", null, props.plugin.lang) : null,
|
|
178
|
+
h("span", null, props.t("updated") + " " + fmtTime(props.plugin.updated))
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function DetailPanel(props) {
|
|
185
|
+
var p = props.plugin;
|
|
186
|
+
var installName = p.fullName.split("/")[1];
|
|
187
|
+
var st = props.installState; // {status, message, pkg} from the settings scope
|
|
188
|
+
var active = st && st.pkg === installName;
|
|
189
|
+
var status = active ? st.status : "idle";
|
|
190
|
+
var explainState = props.explainState; // {status, text} from the settings scope
|
|
191
|
+
var [confirming, setConfirming] = react.useState(false);
|
|
192
|
+
var statusNode = null;
|
|
193
|
+
if (status === "running") {
|
|
194
|
+
statusNode = h("div", { className: "__mp_status" }, props.t("installingHint"));
|
|
195
|
+
} else if (status === "ok") {
|
|
196
|
+
statusNode = h("div", { className: "__mp_status" }, props.t("installOk").replace("{msg}", st.message || ""));
|
|
197
|
+
} else if (status === "error") {
|
|
198
|
+
statusNode = h("div", { className: "__mp_error" }, props.t("installErr").replace("{msg}", st.message || ""));
|
|
199
|
+
}
|
|
200
|
+
var btn;
|
|
201
|
+
if (status === "running") {
|
|
202
|
+
btn = h("button", { type: "button", className: "__mp_more", disabled: true }, props.t("installing"));
|
|
203
|
+
} else if (confirming) {
|
|
204
|
+
btn = h("button", {
|
|
205
|
+
type: "button", className: "__mp_btnPrimary __mp_more",
|
|
206
|
+
onClick: function () { setConfirming(false); props.onInstall(installName); }
|
|
207
|
+
}, props.t("confirmInstall").replace("{pkg}", installName));
|
|
208
|
+
} else {
|
|
209
|
+
btn = h("button", {
|
|
210
|
+
type: "button", className: "__mp_btnPrimary __mp_more",
|
|
211
|
+
onClick: function () { setConfirming(true); }
|
|
212
|
+
}, props.t("installBtn"));
|
|
213
|
+
}
|
|
214
|
+
return h("div", { className: "__mp_detail" },
|
|
215
|
+
h("div", { className: "__mp_detailTitle" }, p.fullName),
|
|
216
|
+
h("div", { className: "__mp_meta" },
|
|
217
|
+
h("span", null, "★ " + p.stars),
|
|
218
|
+
h("span", null, props.t("updated") + " " + fmtTime(p.updated)),
|
|
219
|
+
props.lang ? h("span", null, p.lang) : null
|
|
220
|
+
),
|
|
221
|
+
h("div", null,
|
|
222
|
+
h("a", { className: "__mp_link", href: p.htmlUrl, target: "_blank", rel: "noreferrer" }, props.t("openRepo")),
|
|
223
|
+
" · ",
|
|
224
|
+
h("a", { className: "__mp_link", href: "https://www.npmjs.com/search?q=" + encodeURIComponent(installName), target: "_blank", rel: "noreferrer" }, props.t("openNpm"))
|
|
225
|
+
),
|
|
226
|
+
h("div", { className: "__mp_detailTitle" }, props.t("install")),
|
|
227
|
+
h("div", { className: "__mp_readme" }, props.t("installHint")),
|
|
228
|
+
h("code", { className: "__mp_code" },
|
|
229
|
+
"dsh plugin --profile web add " + installName + "\n" +
|
|
230
|
+
"# then in $DSH_HOME/profiles/web/cordis.patch.yml:\n" +
|
|
231
|
+
"- insert:\n - id: " + installName + "\n name: '" + installName + "'"
|
|
232
|
+
),
|
|
233
|
+
btn,
|
|
234
|
+
statusNode,
|
|
235
|
+
h("div", { className: "__mp_detailTitle" }, props.t("readme")),
|
|
236
|
+
h("div", { className: "__mp_readme" },
|
|
237
|
+
props.readmeLoading ? props.t("loading")
|
|
238
|
+
: props.readmeRateLimited ? props.ghError
|
|
239
|
+
: props.readmeError ? props.t("readmeEmpty")
|
|
240
|
+
: (props.readme || "")
|
|
241
|
+
),
|
|
242
|
+
h("div", { className: "__mp_translateRow" },
|
|
243
|
+
h("button", { type: "button", className: "__mp_translateBtn",
|
|
244
|
+
style: { marginTop: "8px", padding: "4px 10px", fontSize: "12px", cursor: "pointer", background: "var(--dsw-alias-bg-hover, #21262d)", color: "var(--dsw-alias-label-primary, #e6edf3)", border: "1px solid var(--dsw-alias-border, #30363d)", borderRadius: "6px" },
|
|
245
|
+
disabled: explainState && explainState.status === "running",
|
|
246
|
+
onClick: function () { props.onExplain(p.fullName, p.desc, props.readme || ""); }
|
|
247
|
+
}, props.t("aiExplain")),
|
|
248
|
+
h("div", { id: "__mp_translateOut", style: { fontSize: "12px", lineHeight: "1.6", color: "var(--dsw-alias-label-secondary, #8b949e)", whiteSpace: "pre-wrap", wordBreak: "break-word", marginTop: "8px", borderTop: "1px solid var(--dsw-alias-border, #21262d)", paddingTop: "8px" } },
|
|
249
|
+
props.explainError ? h("span", { style: { color: "var(--dsw-alias-state-danger-text, #f85149)" } }, props.explainError)
|
|
250
|
+
: explainState && explainState.status === "running" ? props.t("aiExplaining")
|
|
251
|
+
: explainState && explainState.status === "error" ? props.t("aiExplainErr").replace("{msg}", explainState.text || "unknown")
|
|
252
|
+
: explainState && explainState.status === "ok" ? explainState.text
|
|
253
|
+
: ""
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function MarketplaceSection(props) {
|
|
260
|
+
var t = props.t;
|
|
261
|
+
var scope = props.scope;
|
|
262
|
+
var api = props.api;
|
|
263
|
+
var state = react.useState({ q: "", sort: "stars", page: 1, items: [], loading: false, error: null, total: 0, open: null, readme: null, readmeLoading: false, readmeError: false, readmeRateLimited: false });
|
|
264
|
+
var s = state[0], set = state[1];
|
|
265
|
+
// install-state subscription (stable pattern: useState + subscribe, never
|
|
266
|
+
// depend on getSnapshot() reference identity).
|
|
267
|
+
var [installState, setInstallState] = react.useState(null);
|
|
268
|
+
react.useEffect(function () {
|
|
269
|
+
var alive = true;
|
|
270
|
+
var sync = function () { if (alive) setInstallState(scope.getSnapshot()); };
|
|
271
|
+
sync();
|
|
272
|
+
var un = typeof scope.subscribe === "function" ? scope.subscribe(sync) : null;
|
|
273
|
+
return function () { alive = false; if (un) un(); if (scope.dispose) scope.dispose(); };
|
|
274
|
+
}, [scope]);
|
|
275
|
+
// settings-not-exposed means the host allowlist gate refused the write:
|
|
276
|
+
// explain the restart/manual-fix path instead of showing the raw code.
|
|
277
|
+
var mutateError = function (detail, fallback) {
|
|
278
|
+
if (detail && detail.code === "settings-not-exposed") return t("notExposed");
|
|
279
|
+
return String(detail && (detail.message || detail.code) || fallback);
|
|
280
|
+
};
|
|
281
|
+
var onInstall = react.useCallback(function (pkg) {
|
|
282
|
+
set(function (prev) { return Object.assign({}, prev, { installError: null }); });
|
|
283
|
+
api.settings.mutate({
|
|
284
|
+
ns: "plugin-marketplace",
|
|
285
|
+
ops: [{ op: "set", path: ["install"], value: { pkg: pkg, ts: Date.now() } }]
|
|
286
|
+
}).then(function (response) {
|
|
287
|
+
if (!response.result.ok) {
|
|
288
|
+
var detail = response.result.error || {};
|
|
289
|
+
set(function (prev) { return Object.assign({}, prev, { installError: mutateError(detail, "unknown") }); });
|
|
290
|
+
}
|
|
291
|
+
}).catch(function (e) {
|
|
292
|
+
set(function (prev) { return Object.assign({}, prev, { installError: String(e && e.message || e) }); });
|
|
293
|
+
});
|
|
294
|
+
}, [api, t]);
|
|
295
|
+
// AI-explain request: the host answers over the same settings channel.
|
|
296
|
+
var onExplain = react.useCallback(function (repo, desc, readme) {
|
|
297
|
+
set(function (prev) { return Object.assign({}, prev, { explainError: null }); });
|
|
298
|
+
api.settings.mutate({
|
|
299
|
+
ns: "plugin-marketplace",
|
|
300
|
+
ops: [{ op: "set", path: ["aiExplain"], value: { repo: repo, desc: desc, readme: readme, ts: Date.now() } }]
|
|
301
|
+
}).then(function (response) {
|
|
302
|
+
if (!response.result.ok) {
|
|
303
|
+
var detail = response.result.error || {};
|
|
304
|
+
set(function (prev) { return Object.assign({}, prev, { explainError: mutateError(detail, "unknown") }); });
|
|
305
|
+
}
|
|
306
|
+
}).catch(function (e) {
|
|
307
|
+
set(function (prev) { return Object.assign({}, prev, { explainError: String(e && e.message || e) }); });
|
|
308
|
+
});
|
|
309
|
+
}, [api, t]);
|
|
310
|
+
var load = react.useCallback(function (q, sort, page, append) {
|
|
311
|
+
set(function (prev) { return Object.assign({}, prev, { loading: true, error: null }); });
|
|
312
|
+
fetchPage(q, sort, page).then(function (out) {
|
|
313
|
+
set(function (prev) {
|
|
314
|
+
var items = append ? prev.items.concat(out.items) : out.items;
|
|
315
|
+
return Object.assign({}, prev, { items: items, total: out.total, loading: false, page: page, q: q, sort: sort });
|
|
316
|
+
});
|
|
317
|
+
}).catch(function () {
|
|
318
|
+
set(function (prev) { return Object.assign({}, prev, { loading: false, error: t("ghError") }); });
|
|
319
|
+
});
|
|
320
|
+
}, [t]);
|
|
321
|
+
react.useEffect(function () {
|
|
322
|
+
load("", "stars", 1, false);
|
|
323
|
+
}, [load]);
|
|
324
|
+
var openDetail = react.useCallback(function (plugin) {
|
|
325
|
+
if (s.open && s.open.fullName === plugin.fullName) { set(function (prev) { return Object.assign({}, prev, { open: null }); }); return; }
|
|
326
|
+
set(function (prev) { return Object.assign({}, prev, { open: plugin, readme: null, readmeError: false, readmeRateLimited: false, readmeLoading: true }); });
|
|
327
|
+
// Detail expands inline under the clicked card; scroll minimally
|
|
328
|
+
// (nearest) only when it would fall outside the viewport.
|
|
329
|
+
setTimeout(function () {
|
|
330
|
+
var el = document.querySelector(".__mp_detail");
|
|
331
|
+
if (el) el.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
332
|
+
}, 50);
|
|
333
|
+
fetchReadme(plugin.fullName).then(function (text) {
|
|
334
|
+
set(function (prev) {
|
|
335
|
+
if (!prev.open || prev.open.fullName !== plugin.fullName) return prev;
|
|
336
|
+
return Object.assign({}, prev, { readme: text.slice(0, 1200), readmeLoading: false, readmeError: false, readmeRateLimited: false });
|
|
337
|
+
});
|
|
338
|
+
}).catch(function (err) {
|
|
339
|
+
set(function (prev) {
|
|
340
|
+
if (!prev.open || prev.open.fullName !== plugin.fullName) return prev;
|
|
341
|
+
var rateLimited = err && (err.status === 403 || err.status === 429);
|
|
342
|
+
return Object.assign({}, prev, { readmeLoading: false, readmeError: !rateLimited, readmeRateLimited: rateLimited });
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
}, [s.open]);
|
|
346
|
+
var submit = function (e) {
|
|
347
|
+
e.preventDefault();
|
|
348
|
+
load(s.q, s.sort, 1, false);
|
|
349
|
+
};
|
|
350
|
+
var more = function () { load(s.q, s.sort, s.page + 1, true); };
|
|
351
|
+
return h("div", null,
|
|
352
|
+
h("form", { className: "__mp_toolbar", onSubmit: submit },
|
|
353
|
+
h("input", { className: "__mp_input", type: "search", value: s.q, placeholder: t("search"), onChange: function (e) { set(function (prev) { return Object.assign({}, prev, { q: e.target.value }); }); } }),
|
|
354
|
+
h("select", { className: "__mp_select", value: s.sort, onChange: function (e) { load(s.q, e.target.value, 1, false); } },
|
|
355
|
+
h("option", { value: "stars" }, t("sortStars")),
|
|
356
|
+
h("option", { value: "updated" }, t("sortUpdated"))
|
|
357
|
+
)
|
|
358
|
+
),
|
|
359
|
+
s.total > 0 ? h("p", { className: "__mp_status" }, t("total").replace("{count}", String(s.total))) : null,
|
|
360
|
+
h("ul", { className: "__mp_grid" },
|
|
361
|
+
s.items.map(function (p) {
|
|
362
|
+
// Detail expands INLINE right under the clicked card, so there is
|
|
363
|
+
// no jumping around the page.
|
|
364
|
+
var open = s.open && s.open.fullName === p.fullName;
|
|
365
|
+
return h("div", { key: p.fullName, className: "__mp_item" },
|
|
366
|
+
h(PluginCard, { plugin: p, t: t, onOpen: function () { openDetail(p); } }),
|
|
367
|
+
open ? h(DetailPanel, { plugin: s.open, t: t, readme: s.readme, readmeLoading: s.readmeLoading, readmeError: s.readmeError, readmeRateLimited: s.readmeRateLimited, lang: s.open.lang, installState: installState && installState.status === "ready" ? installState.value.installState : null, onInstall: onInstall, ghError: t("ghError"), explainState: installState && installState.status === "ready" ? installState.value.aiExplainResult : null, explainError: s.explainError, onExplain: onExplain }) : null
|
|
368
|
+
);
|
|
369
|
+
})
|
|
370
|
+
),
|
|
371
|
+
s.loading ? h("p", { className: "__mp_status" }, t("loading")) : null,
|
|
372
|
+
s.error ? h("p", { className: "__mp_error" }, s.error) : null,
|
|
373
|
+
s.items.length === 0 && !s.loading && !s.error ? h("p", { className: "__mp_status" }, t("empty")) : null,
|
|
374
|
+
s.items.length > 0 ? h("button", { type: "button", className: "__mp_more", onClick: more, disabled: s.loading }, t("loadMore")) : null,
|
|
375
|
+
s.installError ? h("p", { className: "__mp_error" }, s.installError) : null
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── plugin ────────────────────────────────────────────────────────────
|
|
380
|
+
function apply(ctx) {
|
|
381
|
+
var t = ctx.locale.bind(NS);
|
|
382
|
+
ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-plugin-marketplace: dictionaries");
|
|
383
|
+
var scope = ctx.settingsScope.bind({ namespace: "plugin-marketplace" });
|
|
384
|
+
var api = ctx.connection.api;
|
|
385
|
+
ctx.slots.inject("settings.section", function () {
|
|
386
|
+
return ctx.slots.register({
|
|
387
|
+
name: "settings.section",
|
|
388
|
+
id: "marketplace",
|
|
389
|
+
order: 20,
|
|
390
|
+
label: function () { return t("nav"); },
|
|
391
|
+
locale: NS
|
|
392
|
+
}, function (props) {
|
|
393
|
+
return h(MarketplaceSection, Object.assign({}, props, { scope: scope, api: api }));
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
exports.apply = apply;
|
|
399
|
+
exports.inject = inject;
|
|
400
|
+
return module.exports;
|
|
401
|
+
}
|
|
402
|
+
});
|