dsh-plugin-discovery 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js ADDED
@@ -0,0 +1,1762 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-plugin-discovery",
3
+ factory(require) {
4
+ const React = require("react");
5
+ const h = React.createElement;
6
+ const NS = "pluginDiscoveryUi";
7
+ const PANEL_ID = "plugin-discovery";
8
+ const PAGE_SIZE = 8;
9
+ const DEFAULT_QUERY = "dsh-pet";
10
+
11
+ const zh = {
12
+ panel: "发现插件",
13
+ intro: "社区插件非 DeepSeek 官方维护,安装前请自行审阅仓库与权限。",
14
+ searchPlaceholder: "搜索关键词,例如:网采、壁纸、dsh",
15
+ search: "搜索",
16
+ searching: "正在加载…",
17
+ searchCardTitle: "关键词搜索",
18
+ searchCardDesc: "按关键词浏览社区候选;安装前会校验是否为 DSH Bundle。",
19
+ sourceAll: "全部",
20
+ sourceNpm: "npm",
21
+ sourceGithub: "GitHub",
22
+ noResults: "未找到相关插件,试试换个关键词。",
23
+ browseHint: "浏览社区候选包;安装前会自动校验是否为 DSH Bundle。",
24
+ install: "安装",
25
+ installing: "安装中…",
26
+ installedBadge: "已安装",
27
+ installingBadge: "安装中",
28
+ notInstalledBadge: "未安装",
29
+ alreadyInstalled: "已安装",
30
+ open: "打开",
31
+ inspectFailed: "无法校验或不是 DSH Bundle",
32
+ resolvePlaceholder: "粘贴完整链接,如 https://github.com/user/repo",
33
+ resolve: "解析安装",
34
+ resolveCardTitle: "链接安装",
35
+ resolveCardDesc: "粘贴已知 npm / GitHub 地址直接解析安装;关键词请用上方搜索。",
36
+ resolveHint: "下方仅用于安装已知仓库;关键词请点上方「搜索」",
37
+ pagePrev: "上一页",
38
+ pageNext: "下一页",
39
+ pageStatus: "第 {page} / {pages} 页 · 共 {total} 项",
40
+ sourceNpmBadge: "npm",
41
+ sourceGithubBadge: "GitHub",
42
+ sourceCatalogBadge: "精选",
43
+ stars: "{n} stars",
44
+ emptyTitle: "还没有列表",
45
+ loadFailed: "加载失败",
46
+ installed: "安装完成,请到「插件」页查看。",
47
+ installProgress: "正在安装 {spec}…",
48
+ installPhase: "安装进度:{phase}",
49
+ installLog: "安装输出:{text}",
50
+ installFailed: "安装失败",
51
+ retry: "重试",
52
+ browseRecommend: "为你推荐",
53
+ browseCategory: "分类浏览",
54
+ browseSearch: "在线搜索",
55
+ recommendCardTitle: "个性化推荐",
56
+ recommendCardDesc: "根据你已安装的插件偏好,从精选目录排序推荐;目录可从 Gitee 自动更新。",
57
+ categoryCardTitle: "按分类浏览",
58
+ categoryCardDesc: "市面常用 DSH 插件收录在 JSON 目录中,优先从 Gitee 拉取最新版。",
59
+ categoryAll: "全部精选",
60
+ catalogMeta: "目录 v{version} · 来源 {origin}",
61
+ catalogOriginBundled: "插件内置",
62
+ catalogOriginCache: "本地缓存",
63
+ catalogOriginGitee: "Gitee",
64
+ catalogOriginGithub: "GitHub",
65
+ catalogOriginRemote: "远程",
66
+ refreshCatalog: "刷新目录",
67
+ reasonFeatured: "精选",
68
+ reasonRelated: "与已装相关",
69
+ reasonPopular: "热门",
70
+ reasonPet: "桌宠爱好者",
71
+ reasonTheme: "主题爱好者",
72
+ reasonIm: "IM 通道",
73
+ reasonMemory: "记忆相关",
74
+ reasonProductivity: "效率相关",
75
+ reasonMedia: "多媒体",
76
+ reasonRemote: "远程相关",
77
+ reasonMarketplace: "市场相关",
78
+ reasonAgent: "智能体相关",
79
+ };
80
+ const en = {
81
+ panel: "Discover",
82
+ intro: "Community plugins are not official; review the repo before installing.",
83
+ searchPlaceholder: "Keywords, e.g. scraping, wallpaper, dsh",
84
+ search: "Search",
85
+ searching: "Loading…",
86
+ searchCardTitle: "Keyword search",
87
+ searchCardDesc: "Browse community candidates; install verifies DSH Bundle metadata.",
88
+ sourceAll: "All",
89
+ sourceNpm: "npm",
90
+ sourceGithub: "GitHub",
91
+ noResults: "No plugins matched. Try another keyword.",
92
+ browseHint: "Browse community packages; install verifies DSH Bundle metadata.",
93
+ install: "Install",
94
+ installing: "Installing…",
95
+ installedBadge: "Installed",
96
+ installingBadge: "Installing",
97
+ notInstalledBadge: "Not installed",
98
+ alreadyInstalled: "Installed",
99
+ open: "Open",
100
+ inspectFailed: "Not a DSH Bundle or inspect failed",
101
+ resolvePlaceholder: "Paste full URL, e.g. https://github.com/user/repo",
102
+ resolve: "Install",
103
+ resolveCardTitle: "Install from link",
104
+ resolveCardDesc: "Paste a known npm / GitHub URL to resolve and install.",
105
+ resolveHint: "Bottom row installs a known repo; use Search above for keywords",
106
+ pagePrev: "Previous",
107
+ pageNext: "Next",
108
+ pageStatus: "Page {page} / {pages} · {total} items",
109
+ sourceNpmBadge: "npm",
110
+ sourceGithubBadge: "GitHub",
111
+ sourceCatalogBadge: "Curated",
112
+ stars: "{n} stars",
113
+ emptyTitle: "Nothing here yet",
114
+ loadFailed: "Failed to load",
115
+ installed: "Install finished. Check the Plugins panel.",
116
+ installProgress: "Installing {spec}…",
117
+ installPhase: "Progress: {phase}",
118
+ installLog: "Output: {text}",
119
+ installFailed: "Install failed",
120
+ retry: "Retry",
121
+ browseRecommend: "For you",
122
+ browseCategory: "Categories",
123
+ browseSearch: "Online search",
124
+ recommendCardTitle: "Personalized picks",
125
+ recommendCardDesc: "Ranked from the curated catalog; auto-updates from Gitee when possible.",
126
+ categoryCardTitle: "Browse by category",
127
+ categoryCardDesc: "Common DSH plugins live in a JSON catalog, preferring Gitee for updates.",
128
+ categoryAll: "All featured",
129
+ catalogMeta: "Catalog v{version} · {origin}",
130
+ catalogOriginBundled: "bundled",
131
+ catalogOriginCache: "local cache",
132
+ catalogOriginGitee: "Gitee",
133
+ catalogOriginGithub: "GitHub",
134
+ catalogOriginRemote: "remote",
135
+ refreshCatalog: "Refresh catalog",
136
+ reasonFeatured: "Featured",
137
+ reasonRelated: "Related to installed",
138
+ reasonPopular: "Popular",
139
+ reasonPet: "Pets fans",
140
+ reasonTheme: "Theme fans",
141
+ reasonIm: "Messaging",
142
+ reasonMemory: "Memory",
143
+ reasonProductivity: "Productivity",
144
+ reasonMedia: "Media",
145
+ reasonRemote: "Remote",
146
+ reasonMarketplace: "Marketplace",
147
+ reasonAgent: "Agents",
148
+ };
149
+
150
+ function format(template, vars) {
151
+ return String(template).replace(/\{(\w+)\}/g, (_, key) => (vars[key] == null ? "" : String(vars[key])));
152
+ }
153
+
154
+ function candidateKeys(item) {
155
+ // 仅保留精确标识,不把 @scope/name 拆成短名,避免 dsh-pet 与 @linxin666/dsh-pet 状态串线
156
+ const keys = [];
157
+ const push = (value) => {
158
+ if (!value) return;
159
+ const text = String(value).trim();
160
+ if (!text || keys.includes(text)) return;
161
+ keys.push(text);
162
+ const bare = text.replace(/^github:/i, "");
163
+ if (bare !== text && !keys.includes(bare)) keys.push(bare);
164
+ };
165
+ push(item.spec);
166
+ push(item.installSpec);
167
+ push(item.title);
168
+ push(item.name);
169
+ push(item.packageName);
170
+ return keys;
171
+ }
172
+
173
+ function isInstalledItem(item, installedNames) {
174
+ if (!installedNames || !installedNames.size) return false;
175
+ return candidateKeys(item).some((key) => installedNames.has(key));
176
+ }
177
+
178
+ const INSTALLED_CACHE_KEY = "dsh.pluginDiscovery.installedNames";
179
+ const JOB_CACHE_KEY = "dsh.pluginDiscovery.installJobs";
180
+
181
+ function readInstalledCache() {
182
+ try {
183
+ const raw = window.localStorage.getItem(INSTALLED_CACHE_KEY);
184
+ const list = raw ? JSON.parse(raw) : [];
185
+ return new Set(Array.isArray(list) ? list.filter(Boolean) : []);
186
+ } catch {
187
+ return new Set();
188
+ }
189
+ }
190
+
191
+ function writeInstalledCache(names) {
192
+ try {
193
+ window.localStorage.setItem(INSTALLED_CACHE_KEY, JSON.stringify([...names]));
194
+ } catch {
195
+ /* ignore */
196
+ }
197
+ }
198
+
199
+ function readJobCache() {
200
+ try {
201
+ const raw = window.localStorage.getItem(JOB_CACHE_KEY);
202
+ const obj = raw ? JSON.parse(raw) : {};
203
+ return obj && typeof obj === "object" ? obj : {};
204
+ } catch {
205
+ return {};
206
+ }
207
+ }
208
+
209
+ function writeJobCache(jobs) {
210
+ try {
211
+ window.localStorage.setItem(JOB_CACHE_KEY, JSON.stringify(jobs));
212
+ } catch {
213
+ /* ignore */
214
+ }
215
+ }
216
+
217
+ function newRequestId() {
218
+ return globalThis.crypto?.randomUUID?.() || `pd-${Date.now()}-${Math.random().toString(16).slice(2)}`;
219
+ }
220
+
221
+ function jobMatchesItem(job, item) {
222
+ if (!job) return false;
223
+ const keys = new Set([...(job.keys || []), job.spec, job.packageName].filter(Boolean));
224
+ return candidateKeys(item).some((key) => keys.has(key));
225
+ }
226
+
227
+ function resolveItemStatus(item, installedNames, jobs) {
228
+ if (isInstalledItem(item, installedNames)) return { status: "installed", job: null };
229
+ const job = Object.values(jobs || {}).find((row) => jobMatchesItem(row, item));
230
+ if (job && job.status === "installing") return { status: "installing", job };
231
+ if (job && job.status === "failed") return { status: "failed", job };
232
+ return { status: "none", job: null };
233
+ }
234
+
235
+ function identitySchema() {
236
+ return {
237
+ parse(value) {
238
+ return value;
239
+ },
240
+ };
241
+ }
242
+
243
+ function strictJson(typeSymbol) {
244
+ return {
245
+ mode: "strict",
246
+ typeSymbol,
247
+ create: identitySchema,
248
+ };
249
+ }
250
+
251
+ function PanelIcon(props) {
252
+ const size = props.size ?? 18;
253
+ return h(
254
+ "svg",
255
+ {
256
+ width: size,
257
+ height: size,
258
+ viewBox: "0 0 24 24",
259
+ fill: "none",
260
+ stroke: "currentColor",
261
+ strokeWidth: "1.8",
262
+ "aria-hidden": "true",
263
+ },
264
+ h("circle", { cx: "11", cy: "11", r: "7" }),
265
+ h("path", { d: "M20 20l-3.5-3.5" }),
266
+ h("path", { d: "M8 11h6M11 8v6" }),
267
+ );
268
+ }
269
+
270
+ const styles = {
271
+ page: {
272
+ boxSizing: "border-box",
273
+ height: "100%",
274
+ fontFamily: "var(--dsw-font-family)",
275
+ color: "var(--dsw-alias-label-primary)",
276
+ display: "flex",
277
+ flexDirection: "column",
278
+ alignItems: "center",
279
+ gap: "24px",
280
+ padding: "0 clamp(24px, 4vw, 48px) 48px",
281
+ overflow: "auto",
282
+ },
283
+ inner: {
284
+ width: "100%",
285
+ maxWidth: "960px",
286
+ display: "flex",
287
+ flexDirection: "column",
288
+ gap: "16px",
289
+ },
290
+ head: {
291
+ paddingTop: "28px",
292
+ display: "flex",
293
+ flexDirection: "column",
294
+ gap: "4px",
295
+ },
296
+ title: {
297
+ margin: 0,
298
+ fontFamily: "var(--dsw-font-family)",
299
+ fontSize: "20px",
300
+ fontWeight: 500,
301
+ lineHeight: "28px",
302
+ color: "var(--dsw-alias-label-primary)",
303
+ },
304
+ intro: {
305
+ margin: 0,
306
+ color: "var(--dsw-alias-label-secondary)",
307
+ fontSize: "13px",
308
+ lineHeight: "20px",
309
+ },
310
+ card: {
311
+ boxSizing: "border-box",
312
+ display: "flex",
313
+ flexDirection: "column",
314
+ gap: "12px",
315
+ padding: "16px 18px",
316
+ borderRadius: "var(--dsw-radius-md, 12px)",
317
+ border: "1px solid var(--dsw-alias-border-l2, var(--dsw-alias-border-l4, #d8dde6))",
318
+ background: "var(--dsw-alias-bg-module-platform, var(--dsw-alias-bg-layer-1, #f5f6f7))",
319
+ boxShadow: "var(--dsw-elevation-soft, var(--dsw-shadow-lv1, 0 2px 8px rgba(0,0,0,0.04)))",
320
+ },
321
+ cardHead: {
322
+ display: "flex",
323
+ flexDirection: "column",
324
+ gap: "2px",
325
+ },
326
+ cardTitle: {
327
+ margin: 0,
328
+ fontSize: "14px",
329
+ fontWeight: 500,
330
+ lineHeight: "22px",
331
+ color: "var(--dsw-alias-label-primary)",
332
+ },
333
+ cardDesc: {
334
+ margin: 0,
335
+ fontSize: "12px",
336
+ fontWeight: 400,
337
+ lineHeight: "18px",
338
+ color: "var(--dsw-alias-label-tertiary)",
339
+ },
340
+ toolbar: {
341
+ display: "flex",
342
+ flexWrap: "wrap",
343
+ gap: "10px",
344
+ alignItems: "center",
345
+ },
346
+ browseTabs: {
347
+ display: "flex",
348
+ flexWrap: "wrap",
349
+ gap: "8px",
350
+ alignItems: "center",
351
+ },
352
+ chips: {
353
+ display: "flex",
354
+ flexWrap: "wrap",
355
+ gap: "8px",
356
+ alignItems: "center",
357
+ },
358
+ chip: {
359
+ display: "inline-flex",
360
+ alignItems: "center",
361
+ height: "28px",
362
+ padding: "0 12px",
363
+ borderRadius: "var(--dsw-radius-sm, 8px)",
364
+ border: "1px solid var(--dsw-alias-border-l2, #d8dde6)",
365
+ background: "var(--dsw-alias-bg-base, #ffffff)",
366
+ color: "var(--dsw-alias-label-secondary)",
367
+ fontSize: "12px",
368
+ fontWeight: 500,
369
+ lineHeight: "18px",
370
+ cursor: "pointer",
371
+ fontFamily: "var(--dsw-font-family)",
372
+ },
373
+ chipActive: {
374
+ borderColor: "var(--dsw-alias-state-business-primary, #4176e6)",
375
+ background: "color-mix(in srgb, var(--dsw-alias-state-business-primary, #4176e6) 12%, transparent)",
376
+ color: "var(--dsw-alias-state-business-primary, #4176e6)",
377
+ },
378
+ searchGrow: {
379
+ flex: "1 1 240px",
380
+ minWidth: "200px",
381
+ },
382
+ field: {
383
+ boxSizing: "border-box",
384
+ width: "100%",
385
+ height: "40px",
386
+ padding: "0 14px",
387
+ borderRadius: "var(--dsw-radius-sm, 8px)",
388
+ border: "1px solid var(--dsw-alias-border-l1, var(--dsw-alias-border-l2, #c8ced6))",
389
+ background: "var(--dsw-alias-bg-base, #ffffff)",
390
+ color: "var(--dsw-alias-label-primary)",
391
+ fontFamily: "var(--dsw-font-family)",
392
+ fontSize: "14px",
393
+ lineHeight: "22px",
394
+ outline: "none",
395
+ boxShadow: "inset 0 1px 2px rgba(16, 24, 40, 0.04)",
396
+ },
397
+ btn: {
398
+ display: "inline-flex",
399
+ alignItems: "center",
400
+ justifyContent: "center",
401
+ height: "40px",
402
+ padding: "0 16px",
403
+ borderRadius: "var(--dsw-radius-sm, 8px)",
404
+ border: "1px solid transparent",
405
+ fontFamily: "var(--dsw-font-family)",
406
+ fontSize: "14px",
407
+ fontWeight: 500,
408
+ lineHeight: "22px",
409
+ cursor: "pointer",
410
+ whiteSpace: "nowrap",
411
+ },
412
+ btnPrimary: {
413
+ background: "var(--dsw-alias-state-business-primary, var(--dsw-static-deepseek-500, #4176e6))",
414
+ color: "#ffffff",
415
+ },
416
+ btnOutline: {
417
+ background: "var(--dsw-alias-bg-base, #ffffff)",
418
+ borderColor: "var(--dsw-alias-border-l1, #c8ced6)",
419
+ color: "var(--dsw-alias-label-primary)",
420
+ },
421
+ btnSm: {
422
+ height: "28px",
423
+ padding: "0 10px",
424
+ fontSize: "12px",
425
+ lineHeight: "18px",
426
+ },
427
+ btnDisabled: {
428
+ opacity: 0.45,
429
+ cursor: "not-allowed",
430
+ },
431
+ seg: {
432
+ display: "inline-flex",
433
+ padding: "3px",
434
+ gap: "2px",
435
+ borderRadius: "var(--dsw-radius-sm, 8px)",
436
+ border: "0.5px solid var(--dsw-alias-border-l2, #d0d5dd)",
437
+ background: "var(--dsw-alias-bg-module-platform, #f5f6f7)",
438
+ },
439
+ segItem: {
440
+ height: "32px",
441
+ padding: "0 12px",
442
+ border: "none",
443
+ borderRadius: "6px",
444
+ background: "transparent",
445
+ color: "var(--dsw-alias-label-secondary)",
446
+ fontFamily: "var(--dsw-font-family)",
447
+ fontSize: "13px",
448
+ cursor: "pointer",
449
+ },
450
+ segItemActive: {
451
+ background: "var(--dsw-alias-bg-layer-2, #ffffff)",
452
+ color: "var(--dsw-alias-label-primary)",
453
+ boxShadow: "0 0 0 0.5px var(--dsw-alias-border-l2, #d0d5dd)",
454
+ fontWeight: 500,
455
+ },
456
+ list: {
457
+ display: "flex",
458
+ flexDirection: "column",
459
+ gap: "8px",
460
+ },
461
+ row: {
462
+ display: "flex",
463
+ alignItems: "flex-start",
464
+ justifyContent: "space-between",
465
+ gap: "16px",
466
+ padding: "14px 16px",
467
+ borderRadius: "var(--dsw-radius-md, 12px)",
468
+ border: "0.5px solid var(--dsw-alias-border-l4, #e5e7eb)",
469
+ background: "var(--dsw-alias-settings-card-fill, var(--dsw-alias-bg-layer-2, #ffffff))",
470
+ boxShadow: "var(--dsw-shadow-lv1, 0 2px 4px 0 rgba(0,0,0,0.05))",
471
+ },
472
+ rowMain: {
473
+ minWidth: 0,
474
+ flex: "1 1 auto",
475
+ display: "grid",
476
+ gap: "4px",
477
+ },
478
+ rowTitle: {
479
+ margin: 0,
480
+ fontFamily: "var(--dsw-font-family)",
481
+ fontSize: "14px",
482
+ fontWeight: 500,
483
+ lineHeight: "22px",
484
+ color: "var(--dsw-alias-label-primary)",
485
+ overflow: "hidden",
486
+ textOverflow: "ellipsis",
487
+ whiteSpace: "nowrap",
488
+ },
489
+ rowDesc: {
490
+ margin: 0,
491
+ color: "var(--dsw-alias-label-secondary)",
492
+ fontSize: "13px",
493
+ lineHeight: "20px",
494
+ display: "-webkit-box",
495
+ WebkitLineClamp: 2,
496
+ WebkitBoxOrient: "vertical",
497
+ overflow: "hidden",
498
+ },
499
+ meta: {
500
+ display: "flex",
501
+ flexWrap: "wrap",
502
+ gap: "8px",
503
+ alignItems: "center",
504
+ color: "var(--dsw-alias-label-tertiary)",
505
+ fontSize: "12px",
506
+ lineHeight: "18px",
507
+ },
508
+ badge: {
509
+ display: "inline-flex",
510
+ alignItems: "center",
511
+ padding: "0 8px",
512
+ height: "20px",
513
+ borderRadius: "999px",
514
+ border: "0.5px solid var(--dsw-alias-border-l2, #d0d5dd)",
515
+ background: "var(--dsw-alias-bg-module-platform, #f5f6f7)",
516
+ fontSize: "11px",
517
+ color: "var(--dsw-alias-label-secondary)",
518
+ },
519
+ badgeInstalled: {
520
+ display: "inline-flex",
521
+ alignItems: "center",
522
+ padding: "0 8px",
523
+ height: "20px",
524
+ borderRadius: "999px",
525
+ border: "0.5px solid var(--dsw-alias-state-success-primary, #4ed17e)",
526
+ background: "var(--dsw-static-green-100, #e6faed)",
527
+ fontSize: "11px",
528
+ color: "var(--dsw-alias-state-success-primary, #2f9e44)",
529
+ },
530
+ badgeInstalling: {
531
+ display: "inline-flex",
532
+ alignItems: "center",
533
+ padding: "0 8px",
534
+ height: "20px",
535
+ borderRadius: "999px",
536
+ border: "0.5px solid var(--dsw-alias-state-business-primary, #4176e6)",
537
+ background: "var(--dsw-static-blue-50, #eff6ff)",
538
+ fontSize: "11px",
539
+ color: "var(--dsw-alias-state-business-primary, #4176e6)",
540
+ },
541
+ btnInstalled: {
542
+ background: "var(--dsw-alias-bg-module-platform, #f5f6f7)",
543
+ borderColor: "var(--dsw-alias-border-l2, #d0d5dd)",
544
+ color: "var(--dsw-alias-label-secondary)",
545
+ cursor: "default",
546
+ },
547
+ btnInstalling: {
548
+ background: "var(--dsw-static-blue-50, #eff6ff)",
549
+ borderColor: "var(--dsw-alias-state-business-primary, #4176e6)",
550
+ color: "var(--dsw-alias-state-business-primary, #4176e6)",
551
+ cursor: "wait",
552
+ },
553
+ actions: {
554
+ display: "flex",
555
+ flexDirection: "column",
556
+ gap: "8px",
557
+ flex: "0 0 auto",
558
+ },
559
+ empty: {
560
+ color: "var(--dsw-alias-label-tertiary)",
561
+ margin: "40px 0",
562
+ fontSize: "13px",
563
+ lineHeight: "20px",
564
+ textAlign: "center",
565
+ },
566
+ status: {
567
+ color: "var(--dsw-alias-label-secondary)",
568
+ fontSize: "13px",
569
+ lineHeight: "20px",
570
+ margin: 0,
571
+ },
572
+ errorBox: {
573
+ display: "flex",
574
+ alignItems: "center",
575
+ justifyContent: "space-between",
576
+ gap: "12px",
577
+ padding: "12px 14px",
578
+ borderRadius: "var(--dsw-radius-sm, 8px)",
579
+ border: "0.5px solid var(--dsw-alias-border-l2, #d0d5dd)",
580
+ background: "var(--dsw-alias-settings-card-fill, #ffffff)",
581
+ color: "var(--dsw-alias-state-warn-primary, var(--dsw-alias-label-secondary))",
582
+ fontSize: "13px",
583
+ },
584
+ pager: {
585
+ display: "flex",
586
+ alignItems: "center",
587
+ justifyContent: "space-between",
588
+ gap: "12px",
589
+ flexWrap: "wrap",
590
+ paddingTop: "4px",
591
+ },
592
+ pagerButtons: {
593
+ display: "flex",
594
+ gap: "8px",
595
+ },
596
+ link: {
597
+ color: "var(--dsw-alias-brand-primary, var(--dsw-alias-label-secondary))",
598
+ textDecoration: "none",
599
+ },
600
+ };
601
+
602
+ function Field(props) {
603
+ return h("input", {
604
+ type: "text",
605
+ style: styles.field,
606
+ value: props.value,
607
+ placeholder: props.placeholder,
608
+ disabled: props.disabled,
609
+ onChange: props.onChange,
610
+ onKeyDown: props.onKeyDown,
611
+ });
612
+ }
613
+
614
+ function Btn(props) {
615
+ const style = Object.assign(
616
+ {},
617
+ styles.btn,
618
+ props.variant === "outline"
619
+ ? styles.btnOutline
620
+ : props.variant === "installed"
621
+ ? styles.btnInstalled
622
+ : props.variant === "installing"
623
+ ? styles.btnInstalling
624
+ : styles.btnPrimary,
625
+ props.size === "sm" ? styles.btnSm : null,
626
+ props.disabled ? styles.btnDisabled : null,
627
+ props.style || null,
628
+ );
629
+ return h(
630
+ "button",
631
+ {
632
+ type: "button",
633
+ style,
634
+ disabled: props.disabled,
635
+ onClick: props.onClick,
636
+ },
637
+ props.children,
638
+ );
639
+ }
640
+
641
+ function SourceSeg(props) {
642
+ return h(
643
+ "div",
644
+ { style: styles.seg, role: "group", "aria-label": "source" },
645
+ props.options.map((opt) =>
646
+ h(
647
+ "button",
648
+ {
649
+ key: opt.value,
650
+ type: "button",
651
+ disabled: props.disabled,
652
+ style: Object.assign(
653
+ {},
654
+ styles.segItem,
655
+ props.value === opt.value ? styles.segItemActive : null,
656
+ props.disabled ? styles.btnDisabled : null,
657
+ ),
658
+ onClick: () => props.onChange(opt.value),
659
+ },
660
+ opt.label,
661
+ ),
662
+ ),
663
+ );
664
+ }
665
+
666
+ function Chip(props) {
667
+ return h(
668
+ "button",
669
+ {
670
+ type: "button",
671
+ disabled: props.disabled,
672
+ style: Object.assign({}, styles.chip, props.active ? styles.chipActive : null),
673
+ onClick: props.onClick,
674
+ },
675
+ props.children,
676
+ );
677
+ }
678
+
679
+ function reasonLabel(t, reason) {
680
+ const keyMap = {
681
+ featured: "reasonFeatured",
682
+ related: "reasonRelated",
683
+ popular: "reasonPopular",
684
+ pet: "reasonPet",
685
+ theme: "reasonTheme",
686
+ im: "reasonIm",
687
+ memory: "reasonMemory",
688
+ productivity: "reasonProductivity",
689
+ media: "reasonMedia",
690
+ remote: "reasonRemote",
691
+ marketplace: "reasonMarketplace",
692
+ agent: "reasonAgent",
693
+ };
694
+ const key = keyMap[reason] || "reasonPopular";
695
+ return t(key);
696
+ }
697
+
698
+ function categoryLabel(cat, lang) {
699
+ if (!cat) return "";
700
+ const label = cat.label || {};
701
+ if (lang === "en") return label.en || label.zh || cat.id;
702
+ return label.zh || label.en || cat.id;
703
+ }
704
+
705
+ function unwrapRemote(result, fallbackMessage) {
706
+ if (!result) return { ok: false, message: fallbackMessage };
707
+ if (result.ok === false) {
708
+ const err = result.error;
709
+ return {
710
+ ok: false,
711
+ message: (err && (err.message || err.code)) || fallbackMessage,
712
+ };
713
+ }
714
+ return result.value;
715
+ }
716
+
717
+ function Panel(props) {
718
+ const api = props.api;
719
+ const t = props.t;
720
+ const lang = React.useMemo(() => {
721
+ const sample = String(t("browseRecommend") || "");
722
+ return /[\u4e00-\u9fff]/.test(sample) ? "zh" : "en";
723
+ }, [t]);
724
+ const [browseMode, setBrowseMode] = React.useState("recommend");
725
+ const [category, setCategory] = React.useState("beginner");
726
+ const [categories, setCategories] = React.useState([]);
727
+ const [catalogInfo, setCatalogInfo] = React.useState({ version: 0, origin: "bundled" });
728
+ const [query, setQuery] = React.useState(DEFAULT_QUERY);
729
+ const [source, setSource] = React.useState("all");
730
+ const [results, setResults] = React.useState([]);
731
+ const [message, setMessage] = React.useState("");
732
+ const [error, setError] = React.useState("");
733
+ const [busy, setBusy] = React.useState(false);
734
+ const [resolveInput, setResolveInput] = React.useState("");
735
+ const [installDetail, setInstallDetail] = React.useState("");
736
+ const [installedNames, setInstalledNames] = React.useState(() => readInstalledCache());
737
+ const [jobs, setJobs] = React.useState(() => readJobCache());
738
+ const [page, setPage] = React.useState(1);
739
+ const jobsRef = React.useRef(jobs);
740
+ const installedNamesRef = React.useRef(installedNames);
741
+ React.useEffect(() => {
742
+ jobsRef.current = jobs;
743
+ writeJobCache(jobs);
744
+ }, [jobs]);
745
+ React.useEffect(() => {
746
+ installedNamesRef.current = installedNames;
747
+ }, [installedNames]);
748
+
749
+ const refreshInstalled = React.useCallback(async () => {
750
+ if (!api || typeof api.listInstalled !== "function") return;
751
+ try {
752
+ const res = await api.listInstalled();
753
+ if (res && res.ok && Array.isArray(res.names)) {
754
+ // 以服务端名单为准整体覆盖,卸载后才能从「已安装」恢复为「未安装」
755
+ const nextNames = new Set();
756
+ for (const name of res.names) {
757
+ if (!name) continue;
758
+ nextNames.add(name);
759
+ }
760
+ writeInstalledCache(nextNames);
761
+ setInstalledNames(nextNames);
762
+
763
+ setJobs((prev) => {
764
+ const next = { ...prev };
765
+ let changed = false;
766
+ for (const [id, job] of Object.entries(next)) {
767
+ const keys = [job.spec, job.packageName, ...(job.keys || [])].filter(Boolean);
768
+ const present = keys.some((key) => nextNames.has(key));
769
+ if (present) {
770
+ if (job.status !== "installed") {
771
+ next[id] = { ...job, status: "installed", detail: "", updatedAt: Date.now() };
772
+ changed = true;
773
+ }
774
+ } else if (job.status === "installed" || job.status === "failed") {
775
+ // 已卸载或过期的终态任务从缓存移除
776
+ delete next[id];
777
+ changed = true;
778
+ }
779
+ }
780
+ return changed ? next : prev;
781
+ });
782
+ }
783
+ } catch {
784
+ /* ignore */
785
+ }
786
+ }, [api]);
787
+
788
+ const markInstalled = React.useCallback((name) => {
789
+ if (!name) return;
790
+ setInstalledNames((prev) => {
791
+ const next = new Set(prev);
792
+ next.add(name);
793
+ writeInstalledCache(next);
794
+ return next;
795
+ });
796
+ }, []);
797
+
798
+ const upsertJob = React.useCallback((requestId, patch) => {
799
+ if (!requestId) return;
800
+ setJobs((prev) => {
801
+ const current = prev[requestId] || { requestId, status: "installing", keys: [] };
802
+ return {
803
+ ...prev,
804
+ [requestId]: {
805
+ ...current,
806
+ ...patch,
807
+ requestId,
808
+ updatedAt: Date.now(),
809
+ },
810
+ };
811
+ });
812
+ }, []);
813
+
814
+ const finishJob = React.useCallback(
815
+ (requestId, outcome) => {
816
+ if (!requestId) return;
817
+ setJobs((prev) => {
818
+ const current = prev[requestId];
819
+ if (!current) return prev;
820
+ if (outcome.status === "installed") {
821
+ for (const key of [current.spec, current.packageName, outcome.packageName, ...(current.keys || [])]) {
822
+ if (key) markInstalled(key);
823
+ }
824
+ }
825
+ return {
826
+ ...prev,
827
+ [requestId]: {
828
+ ...current,
829
+ ...outcome,
830
+ updatedAt: Date.now(),
831
+ },
832
+ };
833
+ });
834
+ },
835
+ [markInstalled],
836
+ );
837
+
838
+ const runCatalog = React.useCallback(
839
+ async (mode, nextCategory) => {
840
+ setBusy(true);
841
+ setMessage("");
842
+ setError("");
843
+ try {
844
+ if (!api || typeof api.listCatalog !== "function") {
845
+ setResults([]);
846
+ setError(t("loadFailed") + ": catalog unavailable");
847
+ return;
848
+ }
849
+ const [res] = await Promise.all([
850
+ api.listCatalog({
851
+ mode: mode === "category" ? "category" : "recommend",
852
+ category: nextCategory || "featured",
853
+ installedNames: [...installedNamesRef.current],
854
+ refresh: false,
855
+ }),
856
+ refreshInstalled(),
857
+ ]);
858
+ if (!res || !res.ok) {
859
+ setResults([]);
860
+ setError((res && res.message) || t("loadFailed"));
861
+ } else {
862
+ if (Array.isArray(res.categories) && res.categories.length) {
863
+ setCategories(res.categories);
864
+ }
865
+ setCatalogInfo({
866
+ version: res.catalogVersion || 0,
867
+ origin: res.catalogOrigin || "bundled",
868
+ });
869
+ const list = res.results || [];
870
+ setResults(list);
871
+ setPage(1);
872
+ if (!list.length) setMessage(t("noResults"));
873
+ }
874
+ } catch (e) {
875
+ setResults([]);
876
+ setError((e && e.message) || String(e));
877
+ } finally {
878
+ setBusy(false);
879
+ }
880
+ },
881
+ [api, refreshInstalled, t],
882
+ );
883
+
884
+ const runSearch = React.useCallback(
885
+ async (nextQuery, nextSource) => {
886
+ setBusy(true);
887
+ setMessage("");
888
+ setError("");
889
+ try {
890
+ if (!api || typeof api.search !== "function") {
891
+ setResults([]);
892
+ setError(t("loadFailed") + ": api unavailable");
893
+ return;
894
+ }
895
+ const [res] = await Promise.all([
896
+ api.search(nextQuery, { source: nextSource, verify: false }),
897
+ refreshInstalled(),
898
+ ]);
899
+ if (!res || !res.ok) {
900
+ setResults([]);
901
+ setError((res && res.message) || t("loadFailed"));
902
+ } else {
903
+ const list = res.results || [];
904
+ setResults(list);
905
+ setPage(1);
906
+ if (!list.length) setMessage(t("noResults"));
907
+ }
908
+ } catch (e) {
909
+ setResults([]);
910
+ setError((e && e.message) || String(e));
911
+ } finally {
912
+ setBusy(false);
913
+ }
914
+ },
915
+ [api, t, refreshInstalled],
916
+ );
917
+
918
+ React.useEffect(() => {
919
+ void refreshInstalled();
920
+ }, [refreshInstalled]);
921
+
922
+ React.useEffect(() => {
923
+ if (!api) return undefined;
924
+ const offs = [];
925
+ if (typeof api.subscribePluginChanged === "function") {
926
+ offs.push(
927
+ api.subscribePluginChanged(() => {
928
+ void refreshInstalled();
929
+ }),
930
+ );
931
+ }
932
+ if (typeof api.subscribeInstallState === "function") {
933
+ offs.push(
934
+ api.subscribeInstallState((progress) => {
935
+ if (!progress || !progress.requestId) return;
936
+ const job = jobsRef.current[progress.requestId];
937
+ if (!job) return;
938
+ const phase = progress.phase || "installing";
939
+ const attempt = progress.attempt
940
+ ? ` · registry ${progress.attempt.index}/${progress.attempt.total}`
941
+ : "";
942
+ upsertJob(progress.requestId, {
943
+ status: "installing",
944
+ detail: `${phase}${attempt}`,
945
+ });
946
+ setInstallDetail(format(t("installPhase"), { phase: `${phase}${attempt}` }));
947
+ }),
948
+ );
949
+ }
950
+ if (typeof api.subscribeInstallLog === "function") {
951
+ offs.push(
952
+ api.subscribeInstallLog((chunk) => {
953
+ if (!chunk || !chunk.requestId) return;
954
+ const job = jobsRef.current[chunk.requestId];
955
+ if (!job) return;
956
+ const text = String(chunk.text || "").trim();
957
+ if (!text) return;
958
+ const line = text.split("\n").filter(Boolean).slice(-1)[0] || text;
959
+ upsertJob(chunk.requestId, {
960
+ status: "installing",
961
+ detail: line.slice(0, 160),
962
+ });
963
+ setInstallDetail(format(t("installLog"), { text: line.slice(0, 160) }));
964
+ }),
965
+ );
966
+ }
967
+ return () => {
968
+ for (const off of offs) {
969
+ try {
970
+ off();
971
+ } catch {
972
+ /* ignore */
973
+ }
974
+ }
975
+ };
976
+ }, [api, refreshInstalled, upsertJob, t]);
977
+
978
+ // 刷新后恢复未完成的安装任务
979
+ React.useEffect(() => {
980
+ if (!api || typeof api.waitInstall !== "function") return undefined;
981
+ let cancelled = false;
982
+ const pending = Object.values(jobsRef.current).filter((job) => job && job.status === "installing" && job.requestId);
983
+ for (const job of pending) {
984
+ void (async () => {
985
+ setInstallDetail(format(t("installProgress"), { spec: job.spec || job.requestId }));
986
+ try {
987
+ const res = await api.waitInstall(job.requestId);
988
+ if (cancelled) return;
989
+ if (res && res.pending) return;
990
+ if (res && res.ok) {
991
+ finishJob(job.requestId, {
992
+ status: "installed",
993
+ packageName: res.packageName || job.packageName,
994
+ detail: res.message || "",
995
+ });
996
+ setMessage(res.message || t("installed"));
997
+ await refreshInstalled();
998
+ } else {
999
+ finishJob(job.requestId, {
1000
+ status: "failed",
1001
+ detail: (res && res.message) || t("installFailed"),
1002
+ });
1003
+ setError((res && res.message) || t("installFailed"));
1004
+ }
1005
+ } catch (e) {
1006
+ if (cancelled) return;
1007
+ finishJob(job.requestId, {
1008
+ status: "failed",
1009
+ detail: (e && e.message) || String(e),
1010
+ });
1011
+ } finally {
1012
+ if (!cancelled) setInstallDetail("");
1013
+ }
1014
+ })();
1015
+ }
1016
+ return () => {
1017
+ cancelled = true;
1018
+ };
1019
+ }, [api, finishJob, refreshInstalled, t]);
1020
+
1021
+ React.useEffect(() => {
1022
+ if (browseMode === "recommend") {
1023
+ void runCatalog("recommend", "featured");
1024
+ } else if (browseMode === "category") {
1025
+ void runCatalog("category", category);
1026
+ }
1027
+ }, [browseMode, category, runCatalog]);
1028
+
1029
+ React.useEffect(() => {
1030
+ if (browseMode !== "search") return;
1031
+ void runSearch(query.trim() || DEFAULT_QUERY, source);
1032
+ }, [browseMode, source, runSearch]);
1033
+
1034
+ const pages = Math.max(1, Math.ceil(results.length / PAGE_SIZE));
1035
+ const safePage = Math.min(page, pages);
1036
+ const pageItems = results.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE);
1037
+
1038
+ const onSearch = () => {
1039
+ setBrowseMode("search");
1040
+ void runSearch(query.trim() || DEFAULT_QUERY, source);
1041
+ };
1042
+
1043
+ const onBrowseMode = (mode) => {
1044
+ setBrowseMode(mode);
1045
+ setMessage("");
1046
+ setError("");
1047
+ };
1048
+
1049
+ const onResolve = async () => {
1050
+ const raw = resolveInput.trim();
1051
+ // 关键词误填到解析框时,自动走搜索
1052
+ if (raw && !/^https?:\/\//i.test(raw) && !/^(github:|gitlab:|bitbucket:)/i.test(raw) && !/^[@a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(raw) && (/\s/.test(raw) || /[\u4e00-\u9fff]/.test(raw))) {
1053
+ setQuery(raw);
1054
+ setResolveInput("");
1055
+ setBrowseMode("search");
1056
+ void runSearch(raw, source);
1057
+ return;
1058
+ }
1059
+ setBusy(true);
1060
+ setMessage("");
1061
+ setError("");
1062
+ try {
1063
+ const res = await api.resolve(raw);
1064
+ if (res && res.ok && res.inspection && res.inspection.bundle) {
1065
+ const item = {
1066
+ spec: res.spec,
1067
+ installSpec: res.spec,
1068
+ title: res.inspection.name || res.spec,
1069
+ description: res.inspection.description || "",
1070
+ source: res.source || "resolve",
1071
+ bundle: true,
1072
+ };
1073
+ setResults([item]);
1074
+ setPage(1);
1075
+ if (isInstalledItem(item, installedNames)) {
1076
+ setMessage(t("alreadyInstalled"));
1077
+ return;
1078
+ }
1079
+ const requestId = newRequestId();
1080
+ upsertJob(requestId, {
1081
+ status: "installing",
1082
+ spec: res.spec,
1083
+ keys: candidateKeys(item),
1084
+ packageName: res.inspection.name || "",
1085
+ detail: "",
1086
+ });
1087
+ setInstallDetail(format(t("installProgress"), { spec: res.spec }));
1088
+ const installed = await api.install(res.spec, { requestId });
1089
+ if (installed && installed.alreadyInstalled) {
1090
+ finishJob(requestId, {
1091
+ status: "installed",
1092
+ packageName: installed.packageName || res.inspection.name || res.spec,
1093
+ detail: installed.message || "",
1094
+ });
1095
+ setMessage(installed.message || t("alreadyInstalled"));
1096
+ } else if (installed && installed.ok) {
1097
+ finishJob(requestId, {
1098
+ status: "installed",
1099
+ packageName: installed.packageName || res.inspection.name || res.spec,
1100
+ detail: installed.message || "",
1101
+ });
1102
+ setMessage(installed.message || t("installed"));
1103
+ await refreshInstalled();
1104
+ } else {
1105
+ finishJob(requestId, {
1106
+ status: "failed",
1107
+ detail: (installed && installed.message) || t("inspectFailed"),
1108
+ });
1109
+ setError((installed && installed.message) || t("inspectFailed"));
1110
+ }
1111
+ } else {
1112
+ setResults([]);
1113
+ setError((res && res.message) || t("inspectFailed"));
1114
+ }
1115
+ } catch (e) {
1116
+ setError((e && e.message) || String(e));
1117
+ } finally {
1118
+ setInstallDetail("");
1119
+ setBusy(false);
1120
+ }
1121
+ };
1122
+
1123
+ const onInstall = async (item) => {
1124
+ const spec = item.installSpec || item.spec;
1125
+ const current = resolveItemStatus(item, installedNames, jobs);
1126
+ if (!spec || current.status === "installed" || current.status === "installing") return;
1127
+ const requestId = newRequestId();
1128
+ upsertJob(requestId, {
1129
+ status: "installing",
1130
+ spec,
1131
+ keys: candidateKeys(item),
1132
+ packageName: item.title || "",
1133
+ detail: "",
1134
+ });
1135
+ setInstallDetail(format(t("installProgress"), { spec }));
1136
+ setMessage("");
1137
+ setError("");
1138
+ try {
1139
+ const res = await api.install(spec, { requestId });
1140
+ if (res && res.alreadyInstalled) {
1141
+ finishJob(requestId, {
1142
+ status: "installed",
1143
+ packageName: res.packageName || item.title || spec,
1144
+ detail: res.message || "",
1145
+ });
1146
+ setMessage(res.message || t("alreadyInstalled"));
1147
+ await refreshInstalled();
1148
+ } else if (res && res.ok) {
1149
+ finishJob(requestId, {
1150
+ status: "installed",
1151
+ packageName: res.packageName || item.title || spec,
1152
+ detail: res.message || "",
1153
+ });
1154
+ for (const key of candidateKeys(item)) markInstalled(key);
1155
+ await refreshInstalled();
1156
+ setMessage(res.message || t("installed"));
1157
+ } else {
1158
+ finishJob(requestId, {
1159
+ status: "failed",
1160
+ detail: (res && res.message) || t("installFailed"),
1161
+ });
1162
+ setError((res && res.message) || t("installFailed"));
1163
+ }
1164
+ } catch (e) {
1165
+ finishJob(requestId, {
1166
+ status: "failed",
1167
+ detail: (e && e.message) || String(e),
1168
+ });
1169
+ setError((e && e.message) || String(e));
1170
+ } finally {
1171
+ setInstallDetail("");
1172
+ }
1173
+ };
1174
+
1175
+ const sourceOptions = [
1176
+ { value: "all", label: t("sourceAll") },
1177
+ { value: "npm", label: t("sourceNpm") },
1178
+ { value: "github", label: t("sourceGithub") },
1179
+ ];
1180
+ const browseOptions = [
1181
+ { value: "recommend", label: t("browseRecommend") },
1182
+ { value: "category", label: t("browseCategory") },
1183
+ { value: "search", label: t("browseSearch") },
1184
+ ];
1185
+ const categoryChips = [
1186
+ { id: "beginner", label: categoryLabel(categories.find((c) => c.id === "beginner"), lang) || (lang === "en" ? "Newbie Essentials" : "新手必装") },
1187
+ { id: "featured", label: t("categoryAll") },
1188
+ ...categories
1189
+ .filter((cat) => cat && cat.id && cat.id !== "featured" && cat.id !== "beginner")
1190
+ .map((cat) => ({ id: cat.id, label: categoryLabel(cat, lang) })),
1191
+ ];
1192
+ const browseCardTitle =
1193
+ browseMode === "recommend"
1194
+ ? t("recommendCardTitle")
1195
+ : browseMode === "category"
1196
+ ? t("categoryCardTitle")
1197
+ : t("searchCardTitle");
1198
+ const browseCardDesc =
1199
+ browseMode === "recommend"
1200
+ ? t("recommendCardDesc")
1201
+ : browseMode === "category"
1202
+ ? t("categoryCardDesc")
1203
+ : t("searchCardDesc");
1204
+ const originKey =
1205
+ catalogInfo.origin === "gitee"
1206
+ ? "catalogOriginGitee"
1207
+ : catalogInfo.origin === "github"
1208
+ ? "catalogOriginGithub"
1209
+ : catalogInfo.origin === "cache"
1210
+ ? "catalogOriginCache"
1211
+ : catalogInfo.origin === "remote"
1212
+ ? "catalogOriginRemote"
1213
+ : "catalogOriginBundled";
1214
+ const onRefreshCatalog = () => {
1215
+ void (async () => {
1216
+ setBusy(true);
1217
+ try {
1218
+ if (api && typeof api.listCatalog === "function") {
1219
+ const res = await api.listCatalog({
1220
+ mode: browseMode === "category" ? "category" : "recommend",
1221
+ category,
1222
+ installedNames: [...installedNamesRef.current],
1223
+ refresh: true,
1224
+ });
1225
+ if (res && res.ok) {
1226
+ if (Array.isArray(res.categories)) setCategories(res.categories);
1227
+ setCatalogInfo({
1228
+ version: res.catalogVersion || 0,
1229
+ origin: res.catalogOrigin || "bundled",
1230
+ });
1231
+ setResults(res.results || []);
1232
+ setPage(1);
1233
+ }
1234
+ }
1235
+ } finally {
1236
+ setBusy(false);
1237
+ }
1238
+ })();
1239
+ };
1240
+
1241
+ return h(
1242
+ "div",
1243
+ { style: styles.page },
1244
+ h(
1245
+ "div",
1246
+ { style: styles.inner },
1247
+ h(
1248
+ "div",
1249
+ { style: styles.head },
1250
+ h("h1", { style: styles.title }, t("panel")),
1251
+ h("p", { style: styles.intro }, t("intro")),
1252
+ ),
1253
+ h(
1254
+ "div",
1255
+ { style: styles.card },
1256
+ h(
1257
+ "div",
1258
+ { style: styles.cardHead },
1259
+ h("h2", { style: styles.cardTitle }, browseCardTitle),
1260
+ h("p", { style: styles.cardDesc }, browseCardDesc),
1261
+ browseMode !== "search"
1262
+ ? h(
1263
+ "p",
1264
+ { style: styles.cardDesc },
1265
+ format(t("catalogMeta"), {
1266
+ version: catalogInfo.version || 0,
1267
+ origin: t(originKey),
1268
+ }),
1269
+ )
1270
+ : null,
1271
+ ),
1272
+ h(
1273
+ "div",
1274
+ { style: styles.browseTabs },
1275
+ h(SourceSeg, {
1276
+ value: browseMode,
1277
+ options: browseOptions,
1278
+ disabled: busy,
1279
+ onChange: onBrowseMode,
1280
+ }),
1281
+ browseMode !== "search"
1282
+ ? h(
1283
+ Btn,
1284
+ {
1285
+ variant: "outline",
1286
+ size: "sm",
1287
+ disabled: busy,
1288
+ onClick: onRefreshCatalog,
1289
+ },
1290
+ t("refreshCatalog"),
1291
+ )
1292
+ : null,
1293
+ ),
1294
+ browseMode === "category"
1295
+ ? h(
1296
+ "div",
1297
+ { style: styles.chips },
1298
+ categoryChips.map((chip) =>
1299
+ h(
1300
+ Chip,
1301
+ {
1302
+ key: chip.id,
1303
+ active: category === chip.id,
1304
+ disabled: busy,
1305
+ onClick: () => setCategory(chip.id),
1306
+ },
1307
+ chip.label,
1308
+ ),
1309
+ ),
1310
+ )
1311
+ : null,
1312
+ browseMode === "search"
1313
+ ? h(
1314
+ "div",
1315
+ { style: styles.toolbar },
1316
+ h(
1317
+ "div",
1318
+ { style: styles.searchGrow },
1319
+ h(Field, {
1320
+ value: query,
1321
+ placeholder: t("searchPlaceholder"),
1322
+ disabled: busy,
1323
+ onChange: (e) => setQuery(e.target.value),
1324
+ onKeyDown: (e) => {
1325
+ if (e.key === "Enter") onSearch();
1326
+ },
1327
+ }),
1328
+ ),
1329
+ h(SourceSeg, {
1330
+ value: source,
1331
+ options: sourceOptions,
1332
+ disabled: busy,
1333
+ onChange: (next) => setSource(next),
1334
+ }),
1335
+ h(Btn, { disabled: busy, onClick: onSearch }, busy ? t("searching") : t("search")),
1336
+ )
1337
+ : null,
1338
+ ),
1339
+ h(
1340
+ "div",
1341
+ { style: styles.card },
1342
+ h(
1343
+ "div",
1344
+ { style: styles.cardHead },
1345
+ h("h2", { style: styles.cardTitle }, t("resolveCardTitle")),
1346
+ h("p", { style: styles.cardDesc }, t("resolveCardDesc")),
1347
+ ),
1348
+ h(
1349
+ "div",
1350
+ { style: styles.toolbar },
1351
+ h(
1352
+ "div",
1353
+ { style: styles.searchGrow },
1354
+ h(Field, {
1355
+ value: resolveInput,
1356
+ placeholder: t("resolvePlaceholder"),
1357
+ disabled: busy,
1358
+ onChange: (e) => setResolveInput(e.target.value),
1359
+ onKeyDown: (e) => {
1360
+ if (e.key === "Enter") void onResolve();
1361
+ },
1362
+ }),
1363
+ ),
1364
+ h(
1365
+ Btn,
1366
+ {
1367
+ variant: "outline",
1368
+ disabled: busy || !resolveInput.trim(),
1369
+ onClick: () => void onResolve(),
1370
+ },
1371
+ t("resolve"),
1372
+ ),
1373
+ ),
1374
+ ),
1375
+ error
1376
+ ? h(
1377
+ "div",
1378
+ { style: styles.errorBox },
1379
+ h("span", null, error),
1380
+ h(
1381
+ Btn,
1382
+ {
1383
+ size: "sm",
1384
+ variant: "outline",
1385
+ disabled: busy,
1386
+ onClick: onSearch,
1387
+ },
1388
+ t("retry"),
1389
+ ),
1390
+ )
1391
+ : null,
1392
+ installDetail ? h("p", { style: styles.status }, installDetail) : null,
1393
+ message && !error ? h("p", { style: styles.status }, message) : null,
1394
+ busy && !results.length
1395
+ ? h("p", { style: styles.empty }, t("searching"))
1396
+ : !results.length
1397
+ ? h("p", { style: styles.empty }, error ? "" : message || t("emptyTitle"))
1398
+ : h(
1399
+ "div",
1400
+ { style: styles.list },
1401
+ pageItems.map((item) => {
1402
+ const spec = item.installSpec || item.spec;
1403
+ const current = resolveItemStatus(item, installedNames, jobs);
1404
+ const already = current.status === "installed";
1405
+ const installing = current.status === "installing";
1406
+ const sourceBadge =
1407
+ item.source === "github"
1408
+ ? t("sourceGithubBadge")
1409
+ : item.catalogId || item.featured != null || item.reason
1410
+ ? t("sourceCatalogBadge")
1411
+ : item.source === "npm"
1412
+ ? t("sourceNpmBadge")
1413
+ : item.source || "";
1414
+ const statusBadge = already
1415
+ ? h("span", { style: styles.badgeInstalled }, t("installedBadge"))
1416
+ : installing
1417
+ ? h("span", { style: styles.badgeInstalling }, t("installingBadge"))
1418
+ : h("span", { style: styles.badge }, t("notInstalledBadge"));
1419
+ const whyBadge = item.reason
1420
+ ? h("span", { style: styles.badge }, reasonLabel(t, item.reason))
1421
+ : null;
1422
+ return h(
1423
+ "div",
1424
+ { key: spec, style: styles.row },
1425
+ h(
1426
+ "div",
1427
+ { style: styles.rowMain },
1428
+ h("h3", { style: styles.rowTitle }, item.title || spec),
1429
+ item.description ? h("p", { style: styles.rowDesc }, item.description) : null,
1430
+ h(
1431
+ "div",
1432
+ { style: styles.meta },
1433
+ sourceBadge ? h("span", { style: styles.badge }, sourceBadge) : null,
1434
+ whyBadge,
1435
+ statusBadge,
1436
+ item.stars != null ? h("span", null, format(t("stars"), { n: item.stars })) : null,
1437
+ item.url
1438
+ ? h(
1439
+ "a",
1440
+ {
1441
+ href: item.url,
1442
+ target: "_blank",
1443
+ rel: "noreferrer",
1444
+ style: styles.link,
1445
+ },
1446
+ t("open"),
1447
+ )
1448
+ : null,
1449
+ ),
1450
+ ),
1451
+ h(
1452
+ "div",
1453
+ { style: styles.actions },
1454
+ h(
1455
+ Btn,
1456
+ {
1457
+ size: "sm",
1458
+ variant: already ? "installed" : installing ? "installing" : "primary",
1459
+ disabled: already || installing || busy,
1460
+ onClick: () => void onInstall(item),
1461
+ },
1462
+ installing
1463
+ ? t("installing")
1464
+ : already
1465
+ ? t("alreadyInstalled")
1466
+ : t("install"),
1467
+ ),
1468
+ ),
1469
+ );
1470
+ }),
1471
+ ),
1472
+ results.length
1473
+ ? h(
1474
+ "div",
1475
+ { style: styles.pager },
1476
+ h(
1477
+ "p",
1478
+ { style: styles.status },
1479
+ format(t("pageStatus"), {
1480
+ page: safePage,
1481
+ pages,
1482
+ total: results.length,
1483
+ }),
1484
+ ),
1485
+ h(
1486
+ "div",
1487
+ { style: styles.pagerButtons },
1488
+ h(
1489
+ Btn,
1490
+ {
1491
+ variant: "outline",
1492
+ size: "sm",
1493
+ disabled: safePage <= 1 || busy,
1494
+ onClick: () => setPage((p) => Math.max(1, p - 1)),
1495
+ },
1496
+ t("pagePrev"),
1497
+ ),
1498
+ h(
1499
+ Btn,
1500
+ {
1501
+ variant: "outline",
1502
+ size: "sm",
1503
+ disabled: safePage >= pages || busy,
1504
+ onClick: () => setPage((p) => Math.min(pages, p + 1)),
1505
+ },
1506
+ t("pageNext"),
1507
+ ),
1508
+ ),
1509
+ )
1510
+ : null,
1511
+ ),
1512
+ );
1513
+ }
1514
+
1515
+ const QUERY_CODEC = strictJson("dsh-plugin-discovery#pluginDiscovery/search:query");
1516
+ const OPTIONS_CODEC = strictJson("dsh-plugin-discovery#pluginDiscovery/search:options");
1517
+ const SEARCH_RESULT = strictJson("dsh-plugin-discovery#pluginDiscovery/search:result");
1518
+ const CATALOG_OPTIONS_CODEC = strictJson("dsh-plugin-discovery#pluginDiscovery/listCatalog:options");
1519
+ const CATALOG_RESULT = strictJson("dsh-plugin-discovery#pluginDiscovery/listCatalog:result");
1520
+ const INPUT_CODEC = strictJson("dsh-plugin-discovery#pluginDiscovery/resolve:input");
1521
+ const RESOLVE_RESULT = strictJson("dsh-plugin-discovery#pluginDiscovery/resolve:result");
1522
+ const SPEC_CODEC = strictJson("dsh-plugin-discovery#pluginDiscovery/installPlugin:spec");
1523
+ const INSTALL_OPTIONS_CODEC = strictJson("dsh-plugin-discovery#pluginDiscovery/installPlugin:options");
1524
+ const INSTALL_RESULT = strictJson("dsh-plugin-discovery#pluginDiscovery/installPlugin:result");
1525
+ const LIST_INSTALLED_RESULT = strictJson("dsh-plugin-discovery#pluginDiscovery/listInstalled:result");
1526
+ const WAIT_REQUEST_ID = strictJson("dsh-plugin-discovery#pluginDiscovery/waitInstall:requestId");
1527
+ const WAIT_RESULT = strictJson("dsh-plugin-discovery#pluginDiscovery/waitInstall:result");
1528
+
1529
+ const TYPERT_REMOTE = {
1530
+ package: "dsh-plugin-discovery",
1531
+ descriptors: [
1532
+ {
1533
+ id: "dsh-plugin-discovery#pluginDiscovery/search",
1534
+ service: "pluginDiscovery",
1535
+ namespace: "pluginDiscovery",
1536
+ method: "search",
1537
+ invocation: { kind: "direct" },
1538
+ parameters: [
1539
+ { name: "query", wire: "query", source: "json", codec: QUERY_CODEC },
1540
+ {
1541
+ name: "options",
1542
+ wire: "options",
1543
+ source: "json",
1544
+ acceptsUndefined: true,
1545
+ codec: OPTIONS_CODEC,
1546
+ },
1547
+ ],
1548
+ result: SEARCH_RESULT,
1549
+ },
1550
+ {
1551
+ id: "dsh-plugin-discovery#pluginDiscovery/listCatalog",
1552
+ service: "pluginDiscovery",
1553
+ namespace: "pluginDiscovery",
1554
+ method: "listCatalog",
1555
+ invocation: { kind: "direct" },
1556
+ parameters: [
1557
+ {
1558
+ name: "options",
1559
+ wire: "options",
1560
+ source: "json",
1561
+ acceptsUndefined: true,
1562
+ codec: CATALOG_OPTIONS_CODEC,
1563
+ },
1564
+ ],
1565
+ result: CATALOG_RESULT,
1566
+ },
1567
+ {
1568
+ id: "dsh-plugin-discovery#pluginDiscovery/resolve",
1569
+ service: "pluginDiscovery",
1570
+ namespace: "pluginDiscovery",
1571
+ method: "resolve",
1572
+ invocation: { kind: "direct" },
1573
+ parameters: [{ name: "input", wire: "input", source: "json", codec: INPUT_CODEC }],
1574
+ result: RESOLVE_RESULT,
1575
+ },
1576
+ {
1577
+ id: "dsh-plugin-discovery#pluginDiscovery/installPlugin",
1578
+ service: "pluginDiscovery",
1579
+ namespace: "pluginDiscovery",
1580
+ method: "installPlugin",
1581
+ invocation: { kind: "direct" },
1582
+ parameters: [
1583
+ { name: "spec", wire: "spec", source: "json", codec: SPEC_CODEC },
1584
+ {
1585
+ name: "options",
1586
+ wire: "options",
1587
+ source: "json",
1588
+ acceptsUndefined: true,
1589
+ codec: INSTALL_OPTIONS_CODEC,
1590
+ },
1591
+ ],
1592
+ result: INSTALL_RESULT,
1593
+ },
1594
+ {
1595
+ id: "dsh-plugin-discovery#pluginDiscovery/listInstalled",
1596
+ service: "pluginDiscovery",
1597
+ namespace: "pluginDiscovery",
1598
+ method: "listInstalled",
1599
+ invocation: { kind: "direct" },
1600
+ parameters: [],
1601
+ result: LIST_INSTALLED_RESULT,
1602
+ },
1603
+ {
1604
+ id: "dsh-plugin-discovery#pluginDiscovery/waitInstall",
1605
+ service: "pluginDiscovery",
1606
+ namespace: "pluginDiscovery",
1607
+ method: "waitInstall",
1608
+ invocation: { kind: "direct" },
1609
+ parameters: [{ name: "requestId", wire: "requestId", source: "json", codec: WAIT_REQUEST_ID }],
1610
+ result: WAIT_RESULT,
1611
+ },
1612
+ ],
1613
+ };
1614
+
1615
+ const inject = ["slots", "locale", "remote"];
1616
+ async function apply(ctx) {
1617
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "plugin-discovery-ui: dictionaries");
1618
+ let mountError = "";
1619
+ try {
1620
+ const unmount = await ctx.remote.$mount(TYPERT_REMOTE);
1621
+ ctx.effect(
1622
+ () => () => {
1623
+ void unmount();
1624
+ },
1625
+ "plugin-discovery-ui: remote",
1626
+ );
1627
+ } catch (error) {
1628
+ mountError = error && error.message ? error.message : String(error);
1629
+ console.error("[plugin-discovery-ui] remote mount failed", error);
1630
+ }
1631
+
1632
+ const remoteNs = () => ctx.get("remote.pluginDiscovery", false);
1633
+ const api = {
1634
+ search: async (q, opts) => {
1635
+ if (mountError) return { ok: false, message: mountError, results: [] };
1636
+ const remote = remoteNs();
1637
+ if (!remote || typeof remote.search !== "function") {
1638
+ return { ok: false, message: "remote.pluginDiscovery.search unavailable", results: [] };
1639
+ }
1640
+ const wire = await remote.search(q, opts);
1641
+ const value = unwrapRemote(wire, "search failed");
1642
+ if (value && value.ok === false && value.message && !value.results) return value;
1643
+ if (wire && wire.ok === false) return { ok: false, message: value.message, results: [] };
1644
+ return value;
1645
+ },
1646
+ listCatalog: async (opts) => {
1647
+ if (mountError) return { ok: false, message: mountError, categories: [], results: [] };
1648
+ const remote = remoteNs();
1649
+ if (!remote || typeof remote.listCatalog !== "function") {
1650
+ return { ok: false, message: "remote.pluginDiscovery.listCatalog unavailable", categories: [], results: [] };
1651
+ }
1652
+ const wire = await remote.listCatalog(opts || {});
1653
+ const value = unwrapRemote(wire, "listCatalog failed");
1654
+ if (value && value.ok === false && value.message && !value.results) return value;
1655
+ if (wire && wire.ok === false) {
1656
+ return { ok: false, message: value.message, categories: [], results: [] };
1657
+ }
1658
+ return value;
1659
+ },
1660
+ resolve: async (input) => {
1661
+ if (mountError) return { ok: false, message: mountError };
1662
+ const remote = remoteNs();
1663
+ if (!remote || typeof remote.resolve !== "function") {
1664
+ return { ok: false, message: "remote.pluginDiscovery.resolve unavailable" };
1665
+ }
1666
+ const wire = await remote.resolve(input);
1667
+ const value = unwrapRemote(wire, "resolve failed");
1668
+ if (wire && wire.ok === false) return { ok: false, message: value.message };
1669
+ return value;
1670
+ },
1671
+ install: async (spec, options) => {
1672
+ if (mountError) return { ok: false, message: mountError };
1673
+ const remote = remoteNs();
1674
+ if (!remote || typeof remote.installPlugin !== "function") {
1675
+ return { ok: false, message: "remote.pluginDiscovery.installPlugin unavailable" };
1676
+ }
1677
+ const wire = await remote.installPlugin(spec, options);
1678
+ const value = unwrapRemote(wire, "install failed");
1679
+ if (wire && wire.ok === false) {
1680
+ return {
1681
+ ok: false,
1682
+ alreadyInstalled: !!(value && value.alreadyInstalled),
1683
+ packageName: value && value.packageName,
1684
+ requestId: value && value.requestId,
1685
+ message: value && value.message,
1686
+ };
1687
+ }
1688
+ return value;
1689
+ },
1690
+ listInstalled: async () => {
1691
+ if (mountError) return { ok: false, names: [] };
1692
+ const remote = remoteNs();
1693
+ if (!remote || typeof remote.listInstalled !== "function") {
1694
+ return { ok: false, names: [] };
1695
+ }
1696
+ const wire = await remote.listInstalled();
1697
+ const value = unwrapRemote(wire, "listInstalled failed");
1698
+ if (wire && wire.ok === false) return { ok: false, names: [] };
1699
+ return value;
1700
+ },
1701
+ waitInstall: async (requestId) => {
1702
+ if (mountError) return { ok: false, message: mountError };
1703
+ const remote = remoteNs();
1704
+ if (!remote || typeof remote.waitInstall !== "function") {
1705
+ return { ok: false, message: "remote.pluginDiscovery.waitInstall unavailable" };
1706
+ }
1707
+ const wire = await remote.waitInstall(requestId);
1708
+ const value = unwrapRemote(wire, "waitInstall failed");
1709
+ if (wire && wire.ok === false) return { ok: false, message: value.message || "waitInstall failed" };
1710
+ return value;
1711
+ },
1712
+ subscribePluginChanged: (listener) => {
1713
+ try {
1714
+ return ctx.remote.$on("plugin-manager/changed", listener);
1715
+ } catch {
1716
+ return () => {};
1717
+ }
1718
+ },
1719
+ subscribeInstallState: (listener) => {
1720
+ try {
1721
+ return ctx.remote.$on("plugin-manager/install-state", listener);
1722
+ } catch {
1723
+ return () => {};
1724
+ }
1725
+ },
1726
+ subscribeInstallLog: (listener) => {
1727
+ try {
1728
+ return ctx.remote.$on("plugin-manager/install-log", listener);
1729
+ } catch {
1730
+ return () => {};
1731
+ }
1732
+ },
1733
+ };
1734
+
1735
+ ctx.slots.inject("main", () =>
1736
+ ctx.slots.register(
1737
+ {
1738
+ name: "main",
1739
+ key: PANEL_ID,
1740
+ locale: NS,
1741
+ inject: () => ({ api }),
1742
+ },
1743
+ Panel,
1744
+ ),
1745
+ );
1746
+ ctx.slots.inject("sidebar.panellist", () =>
1747
+ ctx.slots.register(
1748
+ {
1749
+ name: "sidebar.panellist",
1750
+ id: PANEL_ID,
1751
+ order: 16,
1752
+ locale: NS,
1753
+ label: () => ctx.locale.bind(NS)("panel"),
1754
+ },
1755
+ PanelIcon,
1756
+ ),
1757
+ );
1758
+ }
1759
+
1760
+ return { inject, apply };
1761
+ },
1762
+ });