dsh-plugin-manager-plus 1.3.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/lib/client.js ADDED
@@ -0,0 +1,1240 @@
1
+ /**
2
+ * @file lib/client.js
3
+ * @description DeepSeek Harness (DSH) 插件管理器 (dsh-plugin-manager-plus) 浏览器半边。
4
+ * 在「设置 → 插件」分区分别注册「插件管理」与「插件市场」两个独立标签页。
5
+ * 插件管理支持来源维度与用途维度的胶囊筛选、实时搜索与启停/卸载;
6
+ * 插件市场支持 npm 社区插件搜索与一键安装。
7
+ */
8
+
9
+ window.__ModuleLoader__.load({
10
+ id: "dsh-plugin-manager-plus",
11
+ factory: (require) => {
12
+ const module = { exports: {} };
13
+ const exports = module.exports;
14
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
15
+
16
+ const react = require("react");
17
+ const { jsx: _jsx, jsxs: _jsxs } = require("react/jsx-runtime");
18
+
19
+ const NS = "pluginManager";
20
+
21
+ /** 多语言词典 */
22
+ const zh = {
23
+ "tab.manager": "插件管理",
24
+ "tab.market": "插件市场",
25
+ "installedSection": "已安装插件",
26
+ "marketSection": "社区插件市场",
27
+ "searchInstalled": "搜索已安装插件名称或 ID...",
28
+ "searchMarket": "搜索 npm 社区插件 (如: deepseek, bot, proxy)...",
29
+ "searchBtn": "搜索",
30
+ "installedTag": "已安装",
31
+ "installBtn": "安装",
32
+ "installing": "正在安装...",
33
+ "uninstallBtn": "卸载",
34
+ "uninstalling": "正在卸载...",
35
+ "enableBtn": "启用",
36
+ "disableBtn": "停用",
37
+ "enabledTag": "已启用",
38
+ "disabledTag": "已停用",
39
+ "sourceHome": "用户安装",
40
+ "sourceBuiltin": "官方内置",
41
+ "loading": "正在读取数据...",
42
+ "emptyInstalled": "暂无已安装插件",
43
+ "emptyFiltered": "未找到匹配条件的插件",
44
+ "emptyMarket": "未找到相关社区插件",
45
+ "errorPrefix": "请求失败: ",
46
+ "retry": "重试",
47
+ "refresh": "刷新",
48
+ "confirmInstall": "将安装插件「{name}」并获得宿主完整权限,确认安装?",
49
+ "confirmUninstall": "确定要卸载插件「{name}」吗?",
50
+ "confirmSelfDisable": "你正在停用插件管理器本身!停用后本管理页将立即不可用,且无法在 UI 里恢复——需手动编辑 dsh-home/cordis.patch.yml 移除 disabled 行才能重新启用。确定继续吗?",
51
+ "statusActive": "已挂载",
52
+ "statusLoading": "加载中",
53
+ "statusPending": "等待依赖",
54
+ "statusFailed": "挂载失败",
55
+ "statusUnobserved": "未挂载",
56
+ "builtinTip": "官方内置插件仅支持停用,不支持卸载",
57
+
58
+ // 筛选分类
59
+ "filter.source": "来源",
60
+ "filter.function": "用途",
61
+ "filter.status": "状态",
62
+ "status.all": "全部",
63
+ "status.enabled": "已启用",
64
+ "status.disabled": "已停用",
65
+ "category.all": "全部",
66
+ "category.community": "社区插件",
67
+ "category.core": "核心插件",
68
+ "category.user": "用户插件",
69
+
70
+ "func.all": "全部",
71
+ "func.dev": "编程开发",
72
+ "func.chat": "会话聊天",
73
+ "func.llm": "模型智能",
74
+ "func.ui": "界面增强",
75
+ "func.net": "网络接入",
76
+ "func.infra": "系统底层",
77
+ "func.other": "其他"
78
+ };
79
+
80
+ const en = {
81
+ "tab.manager": "Manage",
82
+ "tab.market": "Market",
83
+ "installedSection": "Installed Plugins",
84
+ "marketSection": "Community Marketplace",
85
+ "searchInstalled": "Search installed plugins...",
86
+ "searchMarket": "Search npm community plugins...",
87
+ "searchBtn": "Search",
88
+ "installedTag": "Installed",
89
+ "installBtn": "Install",
90
+ "installing": "Installing...",
91
+ "uninstallBtn": "Uninstall",
92
+ "uninstalling": "Uninstalling...",
93
+ "enableBtn": "Enable",
94
+ "disableBtn": "Disable",
95
+ "enabledTag": "Enabled",
96
+ "disabledTag": "Disabled",
97
+ "sourceHome": "User",
98
+ "sourceBuiltin": "Built-in",
99
+ "loading": "Loading data...",
100
+ "emptyInstalled": "No plugins installed",
101
+ "emptyFiltered": "No matching plugins found",
102
+ "emptyMarket": "No community plugins found",
103
+ "errorPrefix": "Request failed: ",
104
+ "retry": "Retry",
105
+ "refresh": "Refresh",
106
+ "confirmInstall": "Install plugin \"{name}\"? It will have full host permissions.",
107
+ "confirmUninstall": "Are you sure you want to uninstall \"{name}\"?",
108
+ "confirmSelfDisable": "You are disabling the plugin manager itself! Its UI will go away immediately and cannot be re-enabled from the UI — you would have to edit dsh-home/cordis.patch.yml manually to remove the disabled flag. Continue?",
109
+ "statusActive": "Active",
110
+ "statusLoading": "Loading",
111
+ "statusPending": "Pending",
112
+ "statusFailed": "Failed",
113
+ "statusUnobserved": "Not mounted",
114
+ "builtinTip": "Official built-in plugins can only be disabled, not uninstalled",
115
+
116
+ // Filters
117
+ "filter.source": "Source",
118
+ "filter.function": "Function",
119
+ "filter.status": "Status",
120
+ "status.all": "All",
121
+ "status.enabled": "Enabled",
122
+ "status.disabled": "Disabled",
123
+ "category.all": "All",
124
+ "category.community": "Community",
125
+ "category.core": "Core",
126
+ "category.user": "User",
127
+
128
+ "func.all": "All",
129
+ "func.dev": "Development",
130
+ "func.chat": "Chat & IM",
131
+ "func.llm": "LLM & Models",
132
+ "func.ui": "UI & Display",
133
+ "func.net": "Network & Web",
134
+ "func.infra": "Infrastructure",
135
+ "func.other": "Other"
136
+ };
137
+
138
+ /** 模块短名提取 */
139
+ function moduleShortName(moduleName) {
140
+ if (!moduleName) return "";
141
+ const s = moduleName.startsWith("@") ? moduleName.slice(moduleName.indexOf("/") + 1) : moduleName;
142
+ return s.replace(/^cordis:/, "").replace(/^cordis-plugin-/, "").replace(/^dsh-(?:host-|client-)?/, "");
143
+ }
144
+
145
+ /** 状态小圆点颜色映射 */
146
+ function getPhaseColor(phase) {
147
+ switch (phase) {
148
+ case "active":
149
+ return "var(--dsw-alias-state-success-primary, #10b981)";
150
+ case "loading":
151
+ return "var(--dsw-alias-state-business-primary, #2563eb)";
152
+ case "failed":
153
+ return "var(--dsw-alias-state-error-primary, #ef4444)";
154
+ case "pending":
155
+ default:
156
+ return "var(--dsw-alias-label-tertiary, #9ca3af)";
157
+ }
158
+ }
159
+
160
+ /** 状态文字标签 */
161
+ function getPhaseLabel(phase, t) {
162
+ switch (phase) {
163
+ case "active": return t("statusActive");
164
+ case "loading": return t("statusLoading");
165
+ case "pending": return t("statusPending");
166
+ case "failed": return t("statusFailed");
167
+ default: return t("statusUnobserved");
168
+ }
169
+ }
170
+
171
+ /** 翻译工具函数 */
172
+ function createTranslator() {
173
+ return (key, params) => {
174
+ let text = zh[key] || key;
175
+ if (params) {
176
+ for (const k of Object.keys(params)) {
177
+ text = text.replace(new RegExp(`\\{${k}\\}`, "g"), params[k]);
178
+ }
179
+ }
180
+ return text;
181
+ };
182
+ }
183
+
184
+ /** 来源分类列表定义 */
185
+ const SOURCE_CATEGORIES = [
186
+ { id: "all", labelKey: "category.all" },
187
+ { id: "community", labelKey: "category.community" },
188
+ { id: "core", labelKey: "category.core" },
189
+ { id: "user", labelKey: "category.user" }
190
+ ];
191
+
192
+ /** 用途分类列表定义 */
193
+ const FUNC_CATEGORIES = [
194
+ { id: "all", labelKey: "func.all" },
195
+ { id: "dev", labelKey: "func.dev" },
196
+ { id: "chat", labelKey: "func.chat" },
197
+ { id: "llm", labelKey: "func.llm" },
198
+ { id: "ui", labelKey: "func.ui" },
199
+ { id: "net", labelKey: "func.net" },
200
+ { id: "infra", labelKey: "func.infra" },
201
+ { id: "other", labelKey: "func.other" }
202
+ ];
203
+
204
+ /** 状态(启用/停用)筛选列表定义 */
205
+ const STATUS_CATEGORIES = [
206
+ { id: "all", labelKey: "status.all" },
207
+ { id: "enabled", labelKey: "status.enabled" },
208
+ { id: "disabled", labelKey: "status.disabled" }
209
+ ];
210
+
211
+ /**
212
+ * =========================================================================
213
+ * 1. 插件管理标签页组件 (ManagerTab)
214
+ * =========================================================================
215
+ */
216
+ function ManagerTab() {
217
+ const t = createTranslator();
218
+
219
+ // 状态定义
220
+ const [inventory, setInventory] = react.useState({ loading: true, error: null, entries: [] });
221
+ const [installedQuery, setInstalledQuery] = react.useState("");
222
+ const [sourceFilter, setSourceFilter] = react.useState("all");
223
+ const [funcFilter, setFuncFilter] = react.useState("all");
224
+ const [statusFilter, setStatusFilter] = react.useState("all");
225
+ const [busyTask, setBusyTask] = react.useState(null);
226
+ const [actionLoading, setActionLoading] = react.useState({});
227
+
228
+ // 获取已安装列表
229
+ const fetchInventory = react.useCallback(async () => {
230
+ try {
231
+ const res = await fetch("/plugin-manager/api/inventory");
232
+ const data = await res.json();
233
+ if (data.ok) {
234
+ setInventory({ loading: false, error: null, entries: data.entries || [] });
235
+ } else {
236
+ setInventory(prev => ({ ...prev, loading: false, error: data.error || "获取列表失败" }));
237
+ }
238
+ } catch (e) {
239
+ setInventory(prev => ({ ...prev, loading: false, error: e.message }));
240
+ }
241
+ }, []);
242
+
243
+ react.useEffect(() => {
244
+ fetchInventory();
245
+ }, [fetchInventory]);
246
+
247
+ // 轮询队列状态
248
+ react.useEffect(() => {
249
+ let timer = null;
250
+ let active = true;
251
+
252
+ const checkStatus = async () => {
253
+ try {
254
+ const res = await fetch("/plugin-manager/api/status");
255
+ const data = await res.json();
256
+ if (active && data.ok) {
257
+ if (data.busy) {
258
+ setBusyTask(data.current);
259
+ timer = setTimeout(checkStatus, 1500);
260
+ } else {
261
+ if (busyTask) {
262
+ setBusyTask(null);
263
+ fetchInventory();
264
+ }
265
+ }
266
+ }
267
+ } catch {}
268
+ };
269
+
270
+ if (busyTask) {
271
+ timer = setTimeout(checkStatus, 1500);
272
+ }
273
+
274
+ return () => {
275
+ active = false;
276
+ if (timer) clearTimeout(timer);
277
+ };
278
+ }, [busyTask, fetchInventory]);
279
+
280
+ // 启停切换
281
+ const handleToggle = async (entry) => {
282
+ const id = entry.entryId;
283
+ // 自我保护:停用本插件管理器自身会导致 API 路由随插件卸载而消失,
284
+ // 之后无法在 UI 里再启用(需手改 cordis.patch.yml)。强警告确认。
285
+ if (entry.enabled && (id === "plugin-manager" || id === "plugin-manager-client")) {
286
+ if (!window.confirm(t("confirmSelfDisable"))) return;
287
+ }
288
+ setActionLoading(prev => ({ ...prev, [id]: true }));
289
+ try {
290
+ const res = await fetch("/plugin-manager/api/toggle", {
291
+ method: "POST",
292
+ headers: { "Content-Type": "application/json" },
293
+ body: JSON.stringify({ entryId: id, disabled: entry.enabled })
294
+ });
295
+ const data = await res.json();
296
+ if (data.ok) {
297
+ setInventory(prev => ({
298
+ ...prev,
299
+ entries: prev.entries.map(e => e.entryId === id ? { ...e, enabled: !e.enabled } : e)
300
+ }));
301
+ setTimeout(fetchInventory, 1000);
302
+ } else {
303
+ alert("操作失败: " + (data.error || "未知错误"));
304
+ }
305
+ } catch (e) {
306
+ alert("请求异常: " + e.message);
307
+ } finally {
308
+ setActionLoading(prev => ({ ...prev, [id]: false }));
309
+ }
310
+ };
311
+
312
+ // 卸载插件
313
+ const handleUninstall = async (entry) => {
314
+ const name = entry.moduleName ? moduleShortName(entry.moduleName) : entry.entryId;
315
+ if (!window.confirm(t("confirmUninstall", { name }))) return;
316
+
317
+ const id = entry.entryId;
318
+ setActionLoading(prev => ({ ...prev, [id]: true }));
319
+ setBusyTask({ action: "uninstall", target: id });
320
+
321
+ try {
322
+ const res = await fetch("/plugin-manager/api/uninstall", {
323
+ method: "POST",
324
+ headers: { "Content-Type": "application/json" },
325
+ body: JSON.stringify({ entryId: id })
326
+ });
327
+ const data = await res.json();
328
+ if (data.ok) {
329
+ fetchInventory();
330
+ } else {
331
+ alert("卸载失败: " + (data.error || "未知错误"));
332
+ }
333
+ } catch (e) {
334
+ alert("请求异常: " + e.message);
335
+ } finally {
336
+ setActionLoading(prev => ({ ...prev, [id]: false }));
337
+ }
338
+ };
339
+
340
+ // 统计各分类数量
341
+ const sourceCounts = react.useMemo(() => {
342
+ const counts = { all: inventory.entries.length, community: 0, core: 0, user: 0 };
343
+ for (const entry of inventory.entries) {
344
+ const c = entry.category || "core";
345
+ counts[c] = (counts[c] || 0) + 1;
346
+ }
347
+ return counts;
348
+ }, [inventory.entries]);
349
+
350
+ const funcCounts = react.useMemo(() => {
351
+ const counts = { all: inventory.entries.length, dev: 0, chat: 0, llm: 0, ui: 0, net: 0, infra: 0, other: 0 };
352
+ for (const entry of inventory.entries) {
353
+ const f = entry.funcCategory || "other";
354
+ counts[f] = (counts[f] || 0) + 1;
355
+ }
356
+ return counts;
357
+ }, [inventory.entries]);
358
+
359
+ const statusCounts = react.useMemo(() => {
360
+ const counts = { all: inventory.entries.length, enabled: 0, disabled: 0 };
361
+ for (const entry of inventory.entries) {
362
+ if (entry.enabled) counts.enabled += 1;
363
+ else counts.disabled += 1;
364
+ }
365
+ return counts;
366
+ }, [inventory.entries]);
367
+
368
+ // 综合过滤
369
+ const normalizedQuery = installedQuery.trim().toLowerCase();
370
+ const filteredEntries = react.useMemo(() => {
371
+ return inventory.entries.filter(entry => {
372
+ // 来源筛选
373
+ if (sourceFilter !== "all" && (entry.category || "core") !== sourceFilter) {
374
+ return false;
375
+ }
376
+ // 用途筛选
377
+ if (funcFilter !== "all" && (entry.funcCategory || "other") !== funcFilter) {
378
+ return false;
379
+ }
380
+ // 启用/停用筛选
381
+ if (statusFilter !== "all") {
382
+ const isEnabled = !!entry.enabled;
383
+ if (statusFilter === "enabled" && !isEnabled) return false;
384
+ if (statusFilter === "disabled" && isEnabled) return false;
385
+ }
386
+ // 文本搜索
387
+ if (normalizedQuery) {
388
+ const matchesModule = entry.moduleName && entry.moduleName.toLowerCase().includes(normalizedQuery);
389
+ const matchesId = entry.entryId && entry.entryId.toLowerCase().includes(normalizedQuery);
390
+ if (!matchesModule && !matchesId) return false;
391
+ }
392
+ return true;
393
+ });
394
+ }, [inventory.entries, sourceFilter, funcFilter, statusFilter, normalizedQuery]);
395
+
396
+ return _jsxs("div", {
397
+ style: {
398
+ width: "100%",
399
+ maxWidth: "800px",
400
+ display: "flex",
401
+ flexDirection: "column",
402
+ gap: "18px",
403
+ color: "var(--dsw-alias-label-primary)",
404
+ fontSize: "13px",
405
+ lineHeight: "1.5"
406
+ },
407
+ children: [
408
+ // 顶层 Busy 进度提示条
409
+ busyTask ? _jsxs("div", {
410
+ style: {
411
+ background: "color-mix(in srgb, var(--dsw-alias-state-business-primary) 12%, transparent)",
412
+ border: "1px solid var(--dsw-alias-state-business-primary)",
413
+ borderRadius: "8px",
414
+ padding: "10px 14px",
415
+ display: "flex",
416
+ alignItems: "center",
417
+ gap: "10px",
418
+ color: "var(--dsw-alias-state-business-primary)",
419
+ fontWeight: "500"
420
+ },
421
+ children: [
422
+ _jsx("span", {
423
+ style: {
424
+ display: "inline-block",
425
+ width: "12px",
426
+ height: "12px",
427
+ borderRadius: "50%",
428
+ border: "2px solid var(--dsw-alias-state-business-primary)",
429
+ borderTopColor: "transparent",
430
+ animation: "spin 0.8s linear infinite"
431
+ }
432
+ }),
433
+ _jsx("span", {
434
+ children: busyTask.action === "install"
435
+ ? `正在安装插件「${busyTask.target}」,请稍候...`
436
+ : `正在卸载插件「${busyTask.target}」,请稍候...`
437
+ })
438
+ ]
439
+ }) : null,
440
+
441
+ // ----------------------------------------------------
442
+ // 来源维度筛选 Chips
443
+ // ----------------------------------------------------
444
+ _jsxs("div", {
445
+ style: { display: "flex", flexDirection: "column", gap: "8px" },
446
+ children: [
447
+ _jsxs("div", {
448
+ style: { display: "flex", alignItems: "center", justifyContent: "space-between" },
449
+ children: [
450
+ _jsx("span", {
451
+ style: { fontSize: "12px", fontWeight: "600", color: "var(--dsw-alias-label-secondary)" },
452
+ children: t("filter.source")
453
+ }),
454
+ _jsx("button", {
455
+ type: "button",
456
+ onClick: fetchInventory,
457
+ style: {
458
+ background: "transparent",
459
+ border: "1px solid var(--dsw-alias-border-l2)",
460
+ borderRadius: "6px",
461
+ padding: "2px 8px",
462
+ color: "var(--dsw-alias-label-secondary)",
463
+ fontSize: "11px",
464
+ cursor: "pointer"
465
+ },
466
+ children: t("refresh")
467
+ })
468
+ ]
469
+ }),
470
+ _jsx("div", {
471
+ style: { display: "flex", flexWrap: "wrap", gap: "6px" },
472
+ children: SOURCE_CATEGORIES.map(cat => {
473
+ const active = sourceFilter === cat.id;
474
+ const count = sourceCounts[cat.id] ?? 0;
475
+ return _jsxs("button", {
476
+ type: "button",
477
+ onClick: () => setSourceFilter(cat.id),
478
+ style: {
479
+ borderRadius: "999px",
480
+ fontSize: "12px",
481
+ padding: "3px 10px",
482
+ display: "inline-flex",
483
+ alignItems: "center",
484
+ gap: "5px",
485
+ cursor: "pointer",
486
+ transition: "all 0.15s ease",
487
+ border: active
488
+ ? "1px solid var(--dsw-alias-state-business-primary)"
489
+ : "1px solid var(--dsw-alias-border-l2)",
490
+ color: active
491
+ ? "var(--dsw-alias-state-business-primary)"
492
+ : "var(--dsw-alias-label-tertiary)",
493
+ background: active
494
+ ? "color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent)"
495
+ : "var(--dsw-alias-bg-layer-1)"
496
+ },
497
+ children: [
498
+ _jsx("span", { children: t(cat.labelKey) }),
499
+ _jsxs("span", {
500
+ style: {
501
+ fontSize: "11px",
502
+ opacity: active ? 0.9 : 0.6,
503
+ fontWeight: active ? "600" : "normal"
504
+ },
505
+ children: ["(", count, ")"]
506
+ })
507
+ ]
508
+ }, cat.id);
509
+ })
510
+ })
511
+ ]
512
+ }),
513
+
514
+ // ----------------------------------------------------
515
+ // 用途维度筛选 Chips
516
+ // ----------------------------------------------------
517
+ _jsxs("div", {
518
+ style: { display: "flex", flexDirection: "column", gap: "8px" },
519
+ children: [
520
+ _jsx("span", {
521
+ style: { fontSize: "12px", fontWeight: "600", color: "var(--dsw-alias-label-secondary)" },
522
+ children: t("filter.function")
523
+ }),
524
+ _jsx("div", {
525
+ style: { display: "flex", flexWrap: "wrap", gap: "6px" },
526
+ children: FUNC_CATEGORIES.map(cat => {
527
+ const active = funcFilter === cat.id;
528
+ const count = funcCounts[cat.id] ?? 0;
529
+ return _jsxs("button", {
530
+ type: "button",
531
+ onClick: () => setFuncFilter(cat.id),
532
+ style: {
533
+ borderRadius: "999px",
534
+ fontSize: "12px",
535
+ padding: "3px 10px",
536
+ display: "inline-flex",
537
+ alignItems: "center",
538
+ gap: "5px",
539
+ cursor: "pointer",
540
+ transition: "all 0.15s ease",
541
+ border: active
542
+ ? "1px solid var(--dsw-alias-state-business-primary)"
543
+ : "1px solid var(--dsw-alias-border-l2)",
544
+ color: active
545
+ ? "var(--dsw-alias-state-business-primary)"
546
+ : "var(--dsw-alias-label-tertiary)",
547
+ background: active
548
+ ? "color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent)"
549
+ : "var(--dsw-alias-bg-layer-1)"
550
+ },
551
+ children: [
552
+ _jsx("span", { children: t(cat.labelKey) }),
553
+ _jsxs("span", {
554
+ style: {
555
+ fontSize: "11px",
556
+ opacity: active ? 0.9 : 0.6,
557
+ fontWeight: active ? "600" : "normal"
558
+ },
559
+ children: ["(", count, ")"]
560
+ })
561
+ ]
562
+ }, cat.id);
563
+ })
564
+ })
565
+ ]
566
+ }),
567
+
568
+ // ----------------------------------------------------
569
+ // 状态(启用/停用)维度筛选 Chips
570
+ // ----------------------------------------------------
571
+ _jsxs("div", {
572
+ style: { display: "flex", flexDirection: "column", gap: "8px" },
573
+ children: [
574
+ _jsx("span", {
575
+ style: { fontSize: "12px", fontWeight: "600", color: "var(--dsw-alias-label-secondary)" },
576
+ children: t("filter.status")
577
+ }),
578
+ _jsx("div", {
579
+ style: { display: "flex", flexWrap: "wrap", gap: "6px" },
580
+ children: STATUS_CATEGORIES.map(cat => {
581
+ const active = statusFilter === cat.id;
582
+ const count = statusCounts[cat.id] ?? 0;
583
+ return _jsxs("button", {
584
+ type: "button",
585
+ onClick: () => setStatusFilter(cat.id),
586
+ style: {
587
+ borderRadius: "999px",
588
+ fontSize: "12px",
589
+ padding: "3px 10px",
590
+ display: "inline-flex",
591
+ alignItems: "center",
592
+ gap: "5px",
593
+ cursor: "pointer",
594
+ transition: "all 0.15s ease",
595
+ border: active
596
+ ? (cat.id === "disabled"
597
+ ? "1px solid var(--dsw-alias-state-error-primary)"
598
+ : cat.id === "enabled"
599
+ ? "1px solid var(--dsw-alias-state-success-primary)"
600
+ : "1px solid var(--dsw-alias-state-business-primary)")
601
+ : "1px solid var(--dsw-alias-border-l2)",
602
+ color: active
603
+ ? (cat.id === "disabled"
604
+ ? "var(--dsw-alias-state-error-primary)"
605
+ : cat.id === "enabled"
606
+ ? "var(--dsw-alias-state-success-primary)"
607
+ : "var(--dsw-alias-state-business-primary)")
608
+ : "var(--dsw-alias-label-tertiary)",
609
+ background: active
610
+ ? (cat.id === "disabled"
611
+ ? "color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent)"
612
+ : cat.id === "enabled"
613
+ ? "color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent)"
614
+ : "color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent)")
615
+ : "var(--dsw-alias-bg-layer-1)"
616
+ },
617
+ children: [
618
+ _jsx("span", { children: t(cat.labelKey) }),
619
+ _jsxs("span", {
620
+ style: {
621
+ fontSize: "11px",
622
+ opacity: active ? 0.9 : 0.6,
623
+ fontWeight: active ? "600" : "normal"
624
+ },
625
+ children: ["(", count, ")"]
626
+ })
627
+ ]
628
+ }, cat.id);
629
+ })
630
+ })
631
+ ]
632
+ }),
633
+
634
+ // ----------------------------------------------------
635
+ // 搜索框与列表标题
636
+ // ----------------------------------------------------
637
+ _jsxs("div", {
638
+ style: { display: "flex", flexDirection: "column", gap: "10px" },
639
+ children: [
640
+ _jsx("input", {
641
+ type: "search",
642
+ value: installedQuery,
643
+ onChange: (e) => setInstalledQuery(e.target.value),
644
+ placeholder: t("searchInstalled"),
645
+ style: {
646
+ width: "100%",
647
+ height: "36px",
648
+ padding: "0 12px",
649
+ background: "var(--dsw-alias-bg-layer-1)",
650
+ border: "1px solid var(--dsw-alias-border-l2)",
651
+ borderRadius: "8px",
652
+ color: "var(--dsw-alias-label-primary)",
653
+ outline: "none",
654
+ fontSize: "13px",
655
+ boxSizing: "border-box"
656
+ }
657
+ }),
658
+
659
+ _jsxs("div", {
660
+ style: { display: "flex", alignItems: "baseline", gap: "6px", padding: "0 2px" },
661
+ children: [
662
+ _jsx("span", {
663
+ style: { fontSize: "12px", color: "var(--dsw-alias-label-secondary)" },
664
+ children: "匹配插件"
665
+ }),
666
+ _jsxs("span", {
667
+ style: { fontSize: "12px", fontWeight: "600", color: "var(--dsw-alias-label-primary)" },
668
+ children: [filteredEntries.length, " / ", inventory.entries.length]
669
+ })
670
+ ]
671
+ })
672
+ ]
673
+ }),
674
+
675
+ // ----------------------------------------------------
676
+ // 已安装插件列表内容
677
+ // ----------------------------------------------------
678
+ inventory.loading ? _jsx("div", {
679
+ style: { color: "var(--dsw-alias-label-tertiary)", padding: "16px 0", textAlign: "center" },
680
+ children: t("loading")
681
+ }) : inventory.error ? _jsxs("div", {
682
+ style: { color: "var(--dsw-alias-state-error-primary)", display: "flex", gap: "10px", alignItems: "center" },
683
+ children: [
684
+ _jsx("span", { children: t("errorPrefix") + inventory.error }),
685
+ _jsx("button", {
686
+ type: "button",
687
+ onClick: fetchInventory,
688
+ style: {
689
+ background: "transparent",
690
+ border: "1px solid var(--dsw-alias-border-l2)",
691
+ borderRadius: "6px",
692
+ padding: "2px 8px",
693
+ cursor: "pointer",
694
+ color: "inherit"
695
+ },
696
+ children: t("retry")
697
+ })
698
+ ]
699
+ }) : filteredEntries.length === 0 ? _jsx("div", {
700
+ style: { color: "var(--dsw-alias-label-tertiary)", padding: "24px 0", textAlign: "center" },
701
+ children: inventory.entries.length === 0 ? t("emptyInstalled") : t("emptyFiltered")
702
+ }) : _jsx("div", {
703
+ style: {
704
+ display: "grid",
705
+ gridTemplateColumns: "repeat(auto-fill, minmax(360px, 1fr))",
706
+ gap: "10px"
707
+ },
708
+ children: filteredEntries.map((entry) => {
709
+ const title = entry.moduleName ? moduleShortName(entry.moduleName) : entry.entryId;
710
+ const isBusy = Boolean(actionLoading[entry.entryId]) || (busyTask && busyTask.target === entry.entryId);
711
+ const isUser = entry.category === "user";
712
+ const isCommunity = entry.category === "community";
713
+ const funcKey = `func.${entry.funcCategory || "other"}`;
714
+
715
+ return _jsxs("div", {
716
+ style: {
717
+ background: "var(--dsw-alias-bg-layer-3)",
718
+ border: "1px solid var(--dsw-alias-border-l2)",
719
+ borderRadius: "10px",
720
+ padding: "12px 14px",
721
+ display: "flex",
722
+ flexDirection: "column",
723
+ gap: "8px",
724
+ minWidth: 0
725
+ },
726
+ children: [
727
+ // 头部:标题与状态标签
728
+ _jsxs("div", {
729
+ style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px" },
730
+ children: [
731
+ _jsx("div", {
732
+ style: {
733
+ fontWeight: "600",
734
+ fontSize: "14px",
735
+ overflow: "hidden",
736
+ textOverflow: "ellipsis",
737
+ whiteSpace: "nowrap"
738
+ },
739
+ title: entry.moduleName || entry.entryId,
740
+ children: title
741
+ }),
742
+ _jsxs("div", {
743
+ style: { display: "flex", alignItems: "center", gap: "6px", flexShrink: 0 },
744
+ children: [
745
+ // 挂载点
746
+ entry.enabled ? _jsx("span", {
747
+ title: getPhaseLabel(entry.phase, t),
748
+ style: {
749
+ width: "7px",
750
+ height: "7px",
751
+ borderRadius: "50%",
752
+ background: getPhaseColor(entry.phase),
753
+ display: "inline-block"
754
+ }
755
+ }) : null,
756
+ // 启停状态徽章
757
+ _jsx("span", {
758
+ style: {
759
+ fontSize: "11px",
760
+ padding: "1px 6px",
761
+ borderRadius: "4px",
762
+ background: entry.enabled
763
+ ? "color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent)"
764
+ : "var(--dsw-alias-bg-layer-1)",
765
+ color: entry.enabled
766
+ ? "var(--dsw-alias-state-success-primary)"
767
+ : "var(--dsw-alias-label-tertiary)"
768
+ },
769
+ children: entry.enabled ? t("enabledTag") : t("disabledTag")
770
+ })
771
+ ]
772
+ })
773
+ ]
774
+ }),
775
+
776
+ // 标识与双分类标签
777
+ _jsxs("div", {
778
+ style: { display: "flex", alignItems: "center", justifyContent: "space-between", fontSize: "11px" },
779
+ children: [
780
+ _jsx("code", {
781
+ style: {
782
+ color: "var(--dsw-alias-label-tertiary)",
783
+ fontFamily: "monospace",
784
+ overflow: "hidden",
785
+ textOverflow: "ellipsis",
786
+ whiteSpace: "nowrap",
787
+ maxWidth: "200px"
788
+ },
789
+ title: entry.entryId,
790
+ children: entry.entryId
791
+ }),
792
+ _jsxs("div", {
793
+ style: { display: "flex", alignItems: "center", gap: "5px" },
794
+ children: [
795
+ // 用途徽标
796
+ _jsx("span", {
797
+ style: {
798
+ color: "var(--dsw-alias-label-secondary)",
799
+ background: "var(--dsw-alias-bg-layer-1)",
800
+ padding: "1px 5px",
801
+ borderRadius: "4px"
802
+ },
803
+ children: t(funcKey)
804
+ }),
805
+ // 来源徽标
806
+ _jsx("span", {
807
+ style: {
808
+ color: isUser
809
+ ? "var(--dsw-alias-state-business-primary)"
810
+ : isCommunity
811
+ ? "var(--dsw-alias-state-success-primary)"
812
+ : "var(--dsw-alias-label-tertiary)",
813
+ background: "var(--dsw-alias-bg-layer-1)",
814
+ padding: "1px 5px",
815
+ borderRadius: "4px"
816
+ },
817
+ children: isUser
818
+ ? t("category.user")
819
+ : isCommunity
820
+ ? t("category.community")
821
+ : t("category.core")
822
+ })
823
+ ]
824
+ })
825
+ ]
826
+ }),
827
+
828
+ // 底部操作按钮
829
+ _jsxs("div", {
830
+ style: {
831
+ display: "flex",
832
+ alignItems: "center",
833
+ justifyContent: "flex-end",
834
+ gap: "8px",
835
+ marginTop: "4px",
836
+ paddingTop: "8px",
837
+ borderTop: "1px solid color-mix(in srgb, var(--dsw-alias-border-l2) 40%, transparent)"
838
+ },
839
+ children: [
840
+ // 启停 Toggle 按钮
841
+ _jsx("button", {
842
+ type: "button",
843
+ disabled: isBusy,
844
+ onClick: () => handleToggle(entry),
845
+ style: {
846
+ background: "transparent",
847
+ border: "1px solid var(--dsw-alias-border-l2)",
848
+ borderRadius: "6px",
849
+ padding: "3px 10px",
850
+ fontSize: "12px",
851
+ color: entry.enabled ? "var(--dsw-alias-label-secondary)" : "var(--dsw-alias-state-business-primary)",
852
+ cursor: isBusy ? "not-allowed" : "pointer",
853
+ opacity: isBusy ? 0.6 : 1
854
+ },
855
+ children: entry.enabled ? t("disableBtn") : t("enableBtn")
856
+ }),
857
+
858
+ // 卸载按钮
859
+ entry.manageable ? _jsx("button", {
860
+ type: "button",
861
+ disabled: isBusy,
862
+ onClick: () => handleUninstall(entry),
863
+ style: {
864
+ background: "transparent",
865
+ border: "1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 30%, transparent)",
866
+ borderRadius: "6px",
867
+ padding: "3px 10px",
868
+ fontSize: "12px",
869
+ color: "var(--dsw-alias-state-error-primary)",
870
+ cursor: isBusy ? "not-allowed" : "pointer",
871
+ opacity: isBusy ? 0.6 : 1
872
+ },
873
+ children: t("uninstallBtn")
874
+ }) : null
875
+ ]
876
+ })
877
+ ]
878
+ }, entry.entryId);
879
+ })
880
+ })
881
+ ]
882
+ });
883
+ }
884
+
885
+ /**
886
+ * =========================================================================
887
+ * 2. 插件市场标签页组件 (MarketTab)
888
+ * =========================================================================
889
+ */
890
+ function MarketTab() {
891
+ const t = createTranslator();
892
+
893
+ // 状态定义
894
+ const [market, setMarket] = react.useState({ loading: true, error: null, results: [], query: "" });
895
+ const [marketInput, setMarketInput] = react.useState("");
896
+ const [busyTask, setBusyTask] = react.useState(null);
897
+ const [actionLoading, setActionLoading] = react.useState({});
898
+
899
+ // 搜索市场
900
+ const fetchMarket = react.useCallback(async (keyword = "") => {
901
+ setMarket(prev => ({ ...prev, loading: true, error: null }));
902
+ try {
903
+ const url = `/plugin-manager/api/search?q=${encodeURIComponent(keyword)}`;
904
+ const res = await fetch(url);
905
+ const data = await res.json();
906
+ if (data.ok) {
907
+ setMarket({ loading: false, error: null, results: data.results || [], query: keyword });
908
+ } else {
909
+ setMarket(prev => ({ ...prev, loading: false, error: data.error || "搜索失败" }));
910
+ }
911
+ } catch (e) {
912
+ setMarket(prev => ({ ...prev, loading: false, error: e.message }));
913
+ }
914
+ }, []);
915
+
916
+ react.useEffect(() => {
917
+ fetchMarket("");
918
+ }, [fetchMarket]);
919
+
920
+ // 轮询队列状态
921
+ react.useEffect(() => {
922
+ let timer = null;
923
+ let active = true;
924
+
925
+ const checkStatus = async () => {
926
+ try {
927
+ const res = await fetch("/plugin-manager/api/status");
928
+ const data = await res.json();
929
+ if (active && data.ok) {
930
+ if (data.busy) {
931
+ setBusyTask(data.current);
932
+ timer = setTimeout(checkStatus, 1500);
933
+ } else {
934
+ if (busyTask) {
935
+ setBusyTask(null);
936
+ fetchMarket(market.query);
937
+ }
938
+ }
939
+ }
940
+ } catch {}
941
+ };
942
+
943
+ if (busyTask) {
944
+ timer = setTimeout(checkStatus, 1500);
945
+ }
946
+
947
+ return () => {
948
+ active = false;
949
+ if (timer) clearTimeout(timer);
950
+ };
951
+ }, [busyTask, fetchMarket, market.query]);
952
+
953
+ // 安装插件
954
+ const handleInstall = async (pkg) => {
955
+ if (!window.confirm(t("confirmInstall", { name: pkg.name }))) return;
956
+
957
+ const key = `install-${pkg.name}`;
958
+ setActionLoading(prev => ({ ...prev, [key]: true }));
959
+ setBusyTask({ action: "install", target: pkg.name });
960
+
961
+ try {
962
+ const res = await fetch("/plugin-manager/api/install", {
963
+ method: "POST",
964
+ headers: { "Content-Type": "application/json" },
965
+ body: JSON.stringify({ name: pkg.name, version: pkg.version })
966
+ });
967
+ const data = await res.json();
968
+ if (data.ok) {
969
+ fetchMarket(market.query);
970
+ } else {
971
+ alert("安装失败: " + (data.error || "未知错误"));
972
+ }
973
+ } catch (e) {
974
+ alert("请求异常: " + e.message);
975
+ } finally {
976
+ setActionLoading(prev => ({ ...prev, [key]: false }));
977
+ }
978
+ };
979
+
980
+ return _jsxs("div", {
981
+ style: {
982
+ width: "100%",
983
+ maxWidth: "800px",
984
+ display: "flex",
985
+ flexDirection: "column",
986
+ gap: "16px",
987
+ color: "var(--dsw-alias-label-primary)",
988
+ fontSize: "13px",
989
+ lineHeight: "1.5"
990
+ },
991
+ children: [
992
+ // 顶层 Busy 进度提示条
993
+ busyTask ? _jsxs("div", {
994
+ style: {
995
+ background: "color-mix(in srgb, var(--dsw-alias-state-business-primary) 12%, transparent)",
996
+ border: "1px solid var(--dsw-alias-state-business-primary)",
997
+ borderRadius: "8px",
998
+ padding: "10px 14px",
999
+ display: "flex",
1000
+ alignItems: "center",
1001
+ gap: "10px",
1002
+ color: "var(--dsw-alias-state-business-primary)",
1003
+ fontWeight: "500"
1004
+ },
1005
+ children: [
1006
+ _jsx("span", {
1007
+ style: {
1008
+ display: "inline-block",
1009
+ width: "12px",
1010
+ height: "12px",
1011
+ borderRadius: "50%",
1012
+ border: "2px solid var(--dsw-alias-state-business-primary)",
1013
+ borderTopColor: "transparent",
1014
+ animation: "spin 0.8s linear infinite"
1015
+ }
1016
+ }),
1017
+ _jsx("span", {
1018
+ children: `正在安装插件「${busyTask.target}」,这可能需要 1~2 分钟,请稍候...`
1019
+ })
1020
+ ]
1021
+ }) : null,
1022
+
1023
+ // 头部说明
1024
+ _jsxs("div", {
1025
+ style: { display: "flex", alignItems: "baseline", gap: "8px" },
1026
+ children: [
1027
+ _jsx("h3", {
1028
+ style: { margin: 0, fontSize: "14px", fontWeight: "600" },
1029
+ children: t("marketSection")
1030
+ }),
1031
+ _jsx("span", {
1032
+ style: { color: "var(--dsw-alias-label-tertiary)", fontSize: "12px" },
1033
+ children: "通过 NPM 检索并一键安装社区开发的 DSH 插件"
1034
+ })
1035
+ ]
1036
+ }),
1037
+
1038
+ // 搜索栏
1039
+ _jsxs("form", {
1040
+ onSubmit: (e) => {
1041
+ e.preventDefault();
1042
+ fetchMarket(marketInput);
1043
+ },
1044
+ style: { display: "flex", gap: "8px" },
1045
+ children: [
1046
+ _jsx("input", {
1047
+ type: "search",
1048
+ value: marketInput,
1049
+ onChange: (e) => setMarketInput(e.target.value),
1050
+ placeholder: t("searchMarket"),
1051
+ style: {
1052
+ flex: 1,
1053
+ height: "36px",
1054
+ padding: "0 12px",
1055
+ background: "var(--dsw-alias-bg-layer-1)",
1056
+ border: "1px solid var(--dsw-alias-border-l2)",
1057
+ borderRadius: "8px",
1058
+ color: "var(--dsw-alias-label-primary)",
1059
+ outline: "none",
1060
+ fontSize: "13px",
1061
+ boxSizing: "border-box"
1062
+ }
1063
+ }),
1064
+ _jsx("button", {
1065
+ type: "submit",
1066
+ style: {
1067
+ background: "var(--dsw-alias-state-business-primary)",
1068
+ color: "#ffffff",
1069
+ border: "none",
1070
+ borderRadius: "8px",
1071
+ padding: "0 16px",
1072
+ fontSize: "13px",
1073
+ fontWeight: "500",
1074
+ cursor: "pointer"
1075
+ },
1076
+ children: t("searchBtn")
1077
+ })
1078
+ ]
1079
+ }),
1080
+
1081
+ // 市场列表
1082
+ market.loading ? _jsx("div", {
1083
+ style: { color: "var(--dsw-alias-label-tertiary)", padding: "20px 0", textAlign: "center" },
1084
+ children: t("loading")
1085
+ }) : market.error ? _jsxs("div", {
1086
+ style: { color: "var(--dsw-alias-state-error-primary)", display: "flex", gap: "10px", alignItems: "center" },
1087
+ children: [
1088
+ _jsx("span", { children: t("errorPrefix") + market.error }),
1089
+ _jsx("button", {
1090
+ type: "button",
1091
+ onClick: () => fetchMarket(market.query),
1092
+ style: {
1093
+ background: "transparent",
1094
+ border: "1px solid var(--dsw-alias-border-l2)",
1095
+ borderRadius: "6px",
1096
+ padding: "2px 8px",
1097
+ cursor: "pointer",
1098
+ color: "inherit"
1099
+ },
1100
+ children: t("retry")
1101
+ })
1102
+ ]
1103
+ }) : market.results.length === 0 ? _jsx("div", {
1104
+ style: { color: "var(--dsw-alias-label-tertiary)", padding: "24px 0", textAlign: "center" },
1105
+ children: t("emptyMarket")
1106
+ }) : _jsx("div", {
1107
+ style: { display: "flex", flexDirection: "column", gap: "10px" },
1108
+ children: market.results.map((pkg) => {
1109
+ const isInstalling = Boolean(actionLoading[`install-${pkg.name}`]) || (busyTask && busyTask.target === pkg.name);
1110
+
1111
+ return _jsxs("div", {
1112
+ style: {
1113
+ background: "var(--dsw-alias-bg-layer-3)",
1114
+ border: "1px solid var(--dsw-alias-border-l2)",
1115
+ borderRadius: "10px",
1116
+ padding: "12px 14px",
1117
+ display: "flex",
1118
+ alignItems: "center",
1119
+ justifyContent: "space-between",
1120
+ gap: "16px"
1121
+ },
1122
+ children: [
1123
+ // 左侧信息
1124
+ _jsxs("div", {
1125
+ style: { display: "flex", flexDirection: "column", gap: "4px", minWidth: 0, flex: 1 },
1126
+ children: [
1127
+ _jsxs("div", {
1128
+ style: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" },
1129
+ children: [
1130
+ _jsx("code", {
1131
+ style: {
1132
+ fontWeight: "600",
1133
+ fontSize: "13px",
1134
+ color: "var(--dsw-alias-label-primary)",
1135
+ fontFamily: "monospace"
1136
+ },
1137
+ children: pkg.name
1138
+ }),
1139
+ _jsx("span", {
1140
+ style: {
1141
+ fontSize: "11px",
1142
+ color: "var(--dsw-alias-label-tertiary)",
1143
+ background: "var(--dsw-alias-bg-layer-1)",
1144
+ padding: "1px 5px",
1145
+ borderRadius: "4px"
1146
+ },
1147
+ children: `v${pkg.version}`
1148
+ }),
1149
+ pkg.date ? _jsx("span", {
1150
+ style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary)" },
1151
+ children: pkg.date.slice(0, 10)
1152
+ }) : null
1153
+ ]
1154
+ }),
1155
+ pkg.description ? _jsx("div", {
1156
+ style: {
1157
+ fontSize: "12px",
1158
+ color: "var(--dsw-alias-label-secondary)",
1159
+ overflow: "hidden",
1160
+ display: "-webkit-box",
1161
+ WebkitLineClamp: 2,
1162
+ WebkitBoxOrient: "vertical",
1163
+ lineHeight: "1.4"
1164
+ },
1165
+ children: pkg.description
1166
+ }) : null
1167
+ ]
1168
+ }),
1169
+
1170
+ // 右侧操作
1171
+ _jsx("div", {
1172
+ style: { flexShrink: 0 },
1173
+ children: pkg.installed ? _jsx("span", {
1174
+ style: {
1175
+ fontSize: "12px",
1176
+ padding: "4px 10px",
1177
+ borderRadius: "6px",
1178
+ background: "var(--dsw-alias-bg-layer-1)",
1179
+ color: "var(--dsw-alias-label-tertiary)"
1180
+ },
1181
+ children: t("installedTag")
1182
+ }) : _jsx("button", {
1183
+ type: "button",
1184
+ disabled: isInstalling || Boolean(busyTask),
1185
+ onClick: () => handleInstall(pkg),
1186
+ style: {
1187
+ background: "var(--dsw-alias-state-business-primary)",
1188
+ color: "#ffffff",
1189
+ border: "none",
1190
+ borderRadius: "6px",
1191
+ padding: "5px 12px",
1192
+ fontSize: "12px",
1193
+ fontWeight: "500",
1194
+ cursor: (isInstalling || busyTask) ? "not-allowed" : "pointer",
1195
+ opacity: (isInstalling || busyTask) ? 0.6 : 1
1196
+ },
1197
+ children: isInstalling ? t("installing") : t("installBtn")
1198
+ })
1199
+ })
1200
+ ]
1201
+ }, pkg.name);
1202
+ })
1203
+ })
1204
+ ]
1205
+ });
1206
+ }
1207
+
1208
+ /** 客户端依赖注入 */
1209
+ const inject = ["slots", "locale"];
1210
+
1211
+ /** 插件应用入口 */
1212
+ function apply(ctx) {
1213
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-plugin-manager-plus: dictionaries");
1214
+ const t = ctx.locale.bind(NS);
1215
+
1216
+ // 注册「插件管理」Tab (order: 15)
1217
+ ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
1218
+ name: "settings.plugins.tab",
1219
+ id: "manager",
1220
+ order: 15,
1221
+ label: () => t("tab.manager"),
1222
+ locale: NS
1223
+ }, ManagerTab));
1224
+
1225
+ // 注册「插件市场」Tab (order: 20)
1226
+ ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
1227
+ name: "settings.plugins.tab",
1228
+ id: "market",
1229
+ order: 20,
1230
+ label: () => t("tab.market"),
1231
+ locale: NS
1232
+ }, MarketTab));
1233
+ }
1234
+
1235
+ exports.NS = NS;
1236
+ exports.apply = apply;
1237
+ exports.inject = inject;
1238
+ return module.exports;
1239
+ }
1240
+ });