@sparkelf/dsh-artifact-app 0.0.0-stage → 0.1.1

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,1634 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@sparkelf/dsh-artifact-app",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/CockpitViewer.tsx
10
+ /**
11
+ * Sandboxed cockpit viewer.
12
+ *
13
+ * The iframe is served by the plugin's render route and runs with
14
+ * `sandbox="allow-scripts allow-same-origin"` (opaque origin). The workspace token reaches it only
15
+ * through postMessage, and only after this parent verified the message source.
16
+ *
17
+ * @module client/CockpitViewer
18
+ */
19
+ /**
20
+ * Render routes are served by this DSH host, so the iframe must stay on the same origin.
21
+ * A stale absolute base (`http://localhost:<port>/dsh-artifact-app/render` behind a proxy)
22
+ * would point the frame at a port the browser cannot reach; keep only the pathname.
23
+ */
24
+ const renderBase = (value) => {
25
+ try {
26
+ return new URL(value, window.location.origin).pathname.replace(/\/+$/u, "");
27
+ } catch {
28
+ return value.replace(/\/+$/u, "");
29
+ }
30
+ };
31
+ const frameStyle = {
32
+ width: "100%",
33
+ height: "100%",
34
+ minHeight: "520px",
35
+ border: "0",
36
+ display: "block",
37
+ background: "#060a14"
38
+ };
39
+ /**
40
+ * Render one cockpit in a sandboxed iframe and keep its bootstrap config fed.
41
+ * @param props.artifactId - cockpit (or preview draft) id.
42
+ * @param props.refreshKey - changing it reloads the iframe, used by "updated" events.
43
+ * @param props.reloadToken - bump from outside to force a reload.
44
+ */
45
+ function CockpitViewer({ artifactId, reloadToken }) {
46
+ const frameRef = (0, react.useRef)(null);
47
+ /** iframe 加载完成后重发一次引导配置:`hello` 可能在监听器挂上之前就发出去了。 */
48
+ const resendRef = (0, react.useRef)(null);
49
+ /** `ready` 回执:收到前持续补发 config,覆盖「hello 早于监听器」的竞态。 */
50
+ const ackRef = (0, react.useRef)(false);
51
+ const refreshInFlightRef = (0, react.useRef)(false);
52
+ const [config, setConfig] = (0, react.useState)(null);
53
+ const [token, setToken] = (0, react.useState)(null);
54
+ const [error, setError] = (0, react.useState)(null);
55
+ (0, react.useEffect)(() => {
56
+ let alive = true;
57
+ (async () => {
58
+ try {
59
+ const [configResponse, tokenResponse] = await Promise.all([fetch("/dsh-artifact-app/config", { headers: { accept: "application/json" } }), fetch("/dsh-artifact-app/token", { headers: { accept: "application/json" } })]);
60
+ const configPayload = await configResponse.json();
61
+ const tokenPayload = await tokenResponse.json();
62
+ if (!configResponse.ok) throw new Error(configPayload.error ?? "读取驾驶舱配置失败");
63
+ if (!tokenResponse.ok || typeof tokenPayload.token !== "string" || tokenPayload.token.trim() === "") throw new Error(tokenPayload.error ?? "读取 DataOps 登录凭证失败");
64
+ if (alive) {
65
+ setConfig(configPayload);
66
+ setToken(tokenPayload.token);
67
+ }
68
+ } catch (cause) {
69
+ if (alive) setError(cause instanceof Error ? cause.message : String(cause));
70
+ }
71
+ })();
72
+ return () => {
73
+ alive = false;
74
+ };
75
+ }, []);
76
+ (0, react.useEffect)(() => {
77
+ if (config === null || token === null) return void 0;
78
+ /** 把引导配置推给 iframe;`hello`、`ready` 与 iframe 的 load 事件都会触发它。 */
79
+ const sendConfig = (tokenOverride) => {
80
+ const effectiveToken = tokenOverride ?? token;
81
+ if (effectiveToken === null || effectiveToken === "") return;
82
+ frameRef.current?.contentWindow?.postMessage({
83
+ source: "dataops-artifact-host",
84
+ type: "config",
85
+ payload: {
86
+ artifactId,
87
+ token: effectiveToken,
88
+ gatewayBaseUrl: config.gatewayBaseUrl,
89
+ refreshIntervalSeconds: 300,
90
+ keepConnection: false,
91
+ locale: "zh-CN",
92
+ theme: "dark"
93
+ }
94
+ }, "*");
95
+ };
96
+ /**
97
+ * 401 后重新拉凭证并**直接补发** config。
98
+ * 不能只靠 setToken:刷新拿到的 token 可能与 state 相同(凭据没变),
99
+ * 那样 effect 不会重跑,运行时的 waitForNewConfig 会空等到超时。
100
+ */
101
+ const refreshAndResend = () => {
102
+ if (refreshInFlightRef.current) return;
103
+ refreshInFlightRef.current = true;
104
+ (async () => {
105
+ try {
106
+ const response = await fetch("/dsh-artifact-app/token", {
107
+ headers: { accept: "application/json" },
108
+ cache: "no-store"
109
+ });
110
+ const payload = await response.json();
111
+ if (!response.ok || typeof payload.token !== "string" || payload.token.trim() === "") throw new Error(payload.error ?? "读取 DataOps 登录凭证失败");
112
+ setToken(payload.token);
113
+ console.info("[artifact-app] token refreshed; resending config");
114
+ sendConfig(payload.token);
115
+ } catch (cause) {
116
+ console.error("[artifact-app] token refresh failed", cause);
117
+ setError(cause instanceof Error ? cause.message : String(cause));
118
+ } finally {
119
+ refreshInFlightRef.current = false;
120
+ }
121
+ })();
122
+ };
123
+ /**
124
+ * 判断消息是否来自本 tab 的 iframe。
125
+ *
126
+ * Chrome 对 sandbox iframe 的 `event.source` 与 `iframe.contentWindow` 实测可能
127
+ * 不是同一个代理对象(严格相等为 false),只靠身份比较会把 ready/token:expired 全丢掉。
128
+ * 依次尝试:同窗口代理 → frameElement 归属 → 同源兜底。
129
+ */
130
+ const fromOwnedFrame = (event) => {
131
+ const frame = frameRef.current;
132
+ if (frame === null || event.source === null) return false;
133
+ if (event.source === frame.contentWindow) return true;
134
+ try {
135
+ if (event.source.frameElement === frame) return true;
136
+ } catch {}
137
+ return event.origin === window.location.origin;
138
+ };
139
+ const onMessage = (event) => {
140
+ if (!fromOwnedFrame(event)) return;
141
+ const data = event.data;
142
+ if (data === null || typeof data !== "object" || data.source !== "dataops-artifact-app") return;
143
+ if (data.type === "request-access") {
144
+ const target = config.dataopsWebBaseUrl.replace(/\/+$/u, "");
145
+ window.open(target, "_blank", "noopener,noreferrer");
146
+ return;
147
+ }
148
+ if (data.type === "ready") {
149
+ ackRef.current = true;
150
+ sendConfig();
151
+ return;
152
+ }
153
+ if (data.type === "hello") {
154
+ sendConfig();
155
+ return;
156
+ }
157
+ if (data.type === "token:expired") {
158
+ console.info("[artifact-app] token:expired received; refreshing token");
159
+ refreshAndResend();
160
+ return;
161
+ }
162
+ };
163
+ resendRef.current = sendConfig;
164
+ window.addEventListener("message", onMessage);
165
+ ackRef.current = false;
166
+ sendConfig();
167
+ const retryTimers = [
168
+ 300,
169
+ 900,
170
+ 1800
171
+ ].map((delay) => window.setTimeout(() => {
172
+ if (!ackRef.current) sendConfig();
173
+ }, delay));
174
+ return () => {
175
+ retryTimers.forEach((timer) => window.clearTimeout(timer));
176
+ resendRef.current = null;
177
+ window.removeEventListener("message", onMessage);
178
+ };
179
+ }, [
180
+ artifactId,
181
+ config,
182
+ token
183
+ ]);
184
+ if (error !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
185
+ role: "alert",
186
+ style: {
187
+ padding: 18,
188
+ color: "#b91c1c",
189
+ fontSize: 12
190
+ },
191
+ children: error
192
+ });
193
+ if (config === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
194
+ style: {
195
+ padding: 18,
196
+ color: "var(--dsw-alias-label-secondary, #6b7280)",
197
+ fontSize: 12
198
+ },
199
+ children: "正在准备驾驶舱…"
200
+ });
201
+ if (token === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
202
+ style: {
203
+ padding: 18,
204
+ color: "var(--dsw-alias-label-secondary, #6b7280)",
205
+ fontSize: 12
206
+ },
207
+ children: "正在获取凭据…"
208
+ });
209
+ const src = renderBase(config.renderBaseUrl) + "/" + encodeURIComponent(artifactId) + (reloadToken === void 0 ? "" : "?r=" + String(reloadToken));
210
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
211
+ ref: frameRef,
212
+ title: "驾驶舱",
213
+ src,
214
+ sandbox: "allow-scripts allow-same-origin",
215
+ allow: "fullscreen",
216
+ onLoad: () => resendRef.current?.(),
217
+ style: frameStyle,
218
+ "data-artifact-viewer": artifactId
219
+ });
220
+ }
221
+ //#endregion
222
+ //#region src/client/CockpitTab.tsx
223
+ /**
224
+ * The `dataops-artifact-apps` right-sidebar tab.
225
+ *
226
+ * One pane holds the cockpit catalogue (published apps plus cockpit templates) and, once a card
227
+ * is opened, the sandboxed viewer. Both lists are proxied by the host so the browser holds no
228
+ * DataOps token for reads.
229
+ *
230
+ * @module client/CockpitTab
231
+ */
232
+ const readMeta = (value) => {
233
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
234
+ const record = value;
235
+ return {
236
+ cockpitId: typeof record.cockpitId === "string" ? record.cockpitId : void 0,
237
+ cockpitName: typeof record.cockpitName === "string" ? record.cockpitName : void 0,
238
+ nonce: typeof record.nonce === "string" ? record.nonce : void 0
239
+ };
240
+ };
241
+ const readItems = (payload) => {
242
+ if (Array.isArray(payload)) return payload;
243
+ if (typeof payload === "object" && payload !== null && Array.isArray(payload.items)) return payload.items;
244
+ return [];
245
+ };
246
+ const versionLabel = (row) => {
247
+ const version = row.publishedVersion || row.currentVersion || 1;
248
+ return "v" + String(version);
249
+ };
250
+ /**
251
+ * All styling is local to the tab and consumes the host's `--dsw-*` tokens, so the panel follows
252
+ * both light and dark themes without a second palette.
253
+ */
254
+ const CSS = `
255
+ .dsh-ac {
256
+ display: flex;
257
+ flex-direction: column;
258
+ height: 100%;
259
+ min-height: 0;
260
+ background: var(--dsw-alias-bg-base, #151517);
261
+ color: var(--dsw-alias-label-primary, #f9fafb);
262
+ }
263
+ .dsh-ac__head {
264
+ display: flex;
265
+ align-items: center;
266
+ gap: 8px;
267
+ flex: none;
268
+ padding: 10px 12px;
269
+ border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(255,255,255,.06));
270
+ background: color-mix(in srgb, var(--dsw-alias-bg-base, #151517) 92%, transparent);
271
+ backdrop-filter: blur(10px);
272
+ }
273
+ .dsh-ac__glyph {
274
+ display: grid;
275
+ place-items: center;
276
+ width: 26px;
277
+ height: 26px;
278
+ border-radius: 7px;
279
+ color: var(--dsw-alias-state-business-primary, #7aaaff);
280
+ background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #7aaaff) 14%, transparent);
281
+ flex: none;
282
+ }
283
+ .dsh-ac__heading { flex: 1; min-width: 0; }
284
+ .dsh-ac__name {
285
+ font-size: 12px;
286
+ font-weight: 600;
287
+ line-height: 16px;
288
+ white-space: nowrap;
289
+ overflow: hidden;
290
+ text-overflow: ellipsis;
291
+ }
292
+ .dsh-ac__sub {
293
+ margin-top: 1px;
294
+ font-size: 10px;
295
+ line-height: 14px;
296
+ color: var(--dsw-alias-label-caption, #81858c);
297
+ font-variant-numeric: tabular-nums;
298
+ }
299
+ .dsh-ac__icon {
300
+ display: grid;
301
+ place-items: center;
302
+ width: 26px;
303
+ height: 26px;
304
+ padding: 0;
305
+ border: 0;
306
+ border-radius: 6px;
307
+ background: transparent;
308
+ color: var(--dsw-alias-label-secondary, #cfd3d6);
309
+ cursor: pointer;
310
+ flex: none;
311
+ }
312
+ .dsh-ac__icon:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.08)); color: var(--dsw-alias-label-primary, #f9fafb); }
313
+ .dsh-ac__icon:focus-visible, .dsh-ac__btn:focus-visible, .dsh-ac__card:focus-visible {
314
+ outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary, #7aaaff));
315
+ outline-offset: 1px;
316
+ }
317
+ .dsh-ac__body {
318
+ flex: 1;
319
+ min-height: 0;
320
+ overflow-y: auto;
321
+ padding: 10px;
322
+ display: flex;
323
+ flex-direction: column;
324
+ gap: 10px;
325
+ }
326
+ .dsh-ac__section { display: flex; flex-direction: column; gap: 6px; }
327
+ .dsh-ac__section-head {
328
+ display: flex;
329
+ align-items: center;
330
+ gap: 6px;
331
+ padding: 2px 2px 1px;
332
+ font-size: 11px;
333
+ font-weight: 600;
334
+ color: var(--dsw-alias-label-secondary, #cfd3d6);
335
+ }
336
+ .dsh-ac__count {
337
+ display: inline-flex;
338
+ align-items: center;
339
+ justify-content: center;
340
+ min-width: 16px;
341
+ height: 16px;
342
+ padding: 0 5px;
343
+ border-radius: 999px;
344
+ background: var(--dsw-alias-bg-layer-3, #353638);
345
+ color: var(--dsw-alias-label-caption, #81858c);
346
+ font-size: 10px;
347
+ font-weight: 500;
348
+ font-variant-numeric: tabular-nums;
349
+ }
350
+ .dsh-ac__list { display: flex; flex-direction: column; gap: 6px; }
351
+ .dsh-ac__card {
352
+ position: relative;
353
+ display: flex;
354
+ flex-direction: column;
355
+ gap: 5px;
356
+ min-width: 0;
357
+ padding: 10px 11px;
358
+ border: 1px solid var(--dsw-alias-border-l1, rgba(255,255,255,.06));
359
+ border-radius: 10px;
360
+ background: var(--dsw-alias-bg-layer-2, #2c2c2e);
361
+ color: inherit;
362
+ cursor: pointer;
363
+ transition: border-color 140ms ease, background 140ms ease, transform 140ms ease;
364
+ }
365
+ .dsh-ac__card:hover {
366
+ border-color: var(--dsw-alias-border-l3, rgba(255,255,255,.16));
367
+ background: color-mix(in srgb, var(--dsw-alias-bg-layer-2, #2c2c2e) 82%, var(--dsw-alias-bg-layer-3, #353638));
368
+ transform: translateY(-1px);
369
+ }
370
+ .dsh-ac__card-top { display: flex; align-items: center; gap: 5px; min-width: 0; }
371
+ .dsh-ac__badge {
372
+ display: inline-flex;
373
+ align-items: center;
374
+ height: 18px;
375
+ padding: 0 7px;
376
+ border-radius: 999px;
377
+ font-size: 10px;
378
+ font-weight: 600;
379
+ letter-spacing: .01em;
380
+ flex: none;
381
+ }
382
+ .dsh-ac__badge.is-app {
383
+ color: var(--dsw-alias-state-business-primary, #7aaaff);
384
+ background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #7aaaff) 15%, transparent);
385
+ }
386
+ .dsh-ac__badge.is-component {
387
+ color: var(--dsw-alias-state-success-primary, #22c55e);
388
+ background: color-mix(in srgb, var(--dsw-alias-state-success-primary, #22c55e) 15%, transparent);
389
+ }
390
+ .dsh-ac__badge.is-template {
391
+ /* 模板用「蓝 × 红」两个既有 token 混出紫色,不引入新的硬编码色值。 */
392
+ color: color-mix(in srgb, var(--dsw-alias-state-business-primary, #7aaaff) 55%, var(--dsw-alias-state-error-secondary, #f25a5a));
393
+ background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #7aaaff) 12%, var(--dsw-alias-state-error-secondary, #f25a5a) 10%, transparent);
394
+ }
395
+ .dsh-ac__status {
396
+ display: inline-flex;
397
+ align-items: center;
398
+ gap: 4px;
399
+ margin-left: auto;
400
+ font-size: 10px;
401
+ color: var(--dsw-alias-label-caption, #81858c);
402
+ flex: none;
403
+ }
404
+ .dsh-ac__status::before {
405
+ content: "";
406
+ width: 5px;
407
+ height: 5px;
408
+ border-radius: 50%;
409
+ background: currentColor;
410
+ }
411
+ .dsh-ac__status.is-published { color: var(--dsw-alias-state-success-primary, #22c55e); }
412
+ .dsh-ac__status.is-draft { color: var(--dsw-alias-state-warn-primary, #f59e0b); }
413
+ .dsh-ac__card-title {
414
+ margin: 0;
415
+ font-size: 12px;
416
+ font-weight: 600;
417
+ line-height: 17px;
418
+ color: var(--dsw-alias-label-primary, #f9fafb);
419
+ overflow: hidden;
420
+ text-overflow: ellipsis;
421
+ white-space: nowrap;
422
+ }
423
+ .dsh-ac__card-desc {
424
+ margin: 0;
425
+ font-size: 10px;
426
+ line-height: 14px;
427
+ color: var(--dsw-alias-label-secondary, #cfd3d6);
428
+ display: -webkit-box;
429
+ -webkit-line-clamp: 2;
430
+ -webkit-box-orient: vertical;
431
+ overflow: hidden;
432
+ }
433
+ .dsh-ac__meta {
434
+ display: flex;
435
+ align-items: center;
436
+ flex-wrap: wrap;
437
+ gap: 6px;
438
+ font-size: 10px;
439
+ color: var(--dsw-alias-label-caption, #81858c);
440
+ font-variant-numeric: tabular-nums;
441
+ }
442
+ .dsh-ac__meta .dot { width: 2px; height: 2px; border-radius: 50%; background: currentColor; opacity: .7; }
443
+ .dsh-ac__actions {
444
+ display: flex;
445
+ align-items: center;
446
+ gap: 6px;
447
+ margin-top: 2px;
448
+ }
449
+ .dsh-ac__btn {
450
+ appearance: none;
451
+ display: inline-flex;
452
+ align-items: center;
453
+ justify-content: center;
454
+ gap: 4px;
455
+ height: 24px;
456
+ padding: 0 9px;
457
+ border: 1px solid var(--dsw-alias-border-l3, rgba(255,255,255,.16));
458
+ border-radius: 6px;
459
+ background: transparent;
460
+ color: var(--dsw-alias-label-primary, #f9fafb);
461
+ font-size: 10px;
462
+ font-weight: 500;
463
+ cursor: pointer;
464
+ transition: background 120ms ease, border-color 120ms ease;
465
+ }
466
+ .dsh-ac__btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.08)); }
467
+ .dsh-ac__btn.is-primary {
468
+ border-color: var(--dsw-alias-state-business-primary, #7aaaff);
469
+ background: var(--dsw-alias-state-business-primary, #7aaaff);
470
+ color: var(--dsw-alias-label-primary-foreground, #0f1115);
471
+ }
472
+ .dsh-ac__btn.is-primary:hover { background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #7aaaff) 86%, white); }
473
+ .dsh-ac__btn:disabled { opacity: .45; cursor: not-allowed; }
474
+ .dsh-ac__state {
475
+ padding: 22px 12px;
476
+ text-align: center;
477
+ font-size: 11px;
478
+ color: var(--dsw-alias-label-caption, #81858c);
479
+ }
480
+ .dsh-ac__error {
481
+ padding: 8px 10px;
482
+ border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary, #f25a5a) 35%, transparent);
483
+ border-radius: 8px;
484
+ background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #f25a5a) 9%, transparent);
485
+ color: var(--dsw-alias-state-error-primary, #f25a5a);
486
+ font-size: 10px;
487
+ line-height: 15px;
488
+ }
489
+ .dsh-ac__toast {
490
+ position: absolute;
491
+ left: 10px;
492
+ right: 10px;
493
+ bottom: 10px;
494
+ z-index: 5;
495
+ padding: 6px 10px;
496
+ border: 1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12));
497
+ border-radius: 7px;
498
+ background: var(--dsw-alias-bg-layer-3, #353638);
499
+ color: var(--dsw-alias-label-primary, #f9fafb);
500
+ font-size: 10px;
501
+ text-align: center;
502
+ box-shadow: 0 8px 24px rgba(0,0,0,.28);
503
+ pointer-events: none;
504
+ }
505
+ .dsh-ac-viewer { display: flex; flex-direction: column; height: 100%; min-height: 0; }
506
+ .dsh-ac-viewer__head {
507
+ display: flex;
508
+ align-items: center;
509
+ gap: 8px;
510
+ flex: none;
511
+ padding: 8px 10px;
512
+ border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(255,255,255,.06));
513
+ }
514
+ .dsh-ac-viewer__back {
515
+ border: none;
516
+ background: transparent;
517
+ color: var(--dsw-alias-label-secondary, #cfd3d6);
518
+ cursor: pointer;
519
+ font-size: 11px;
520
+ padding: 2px 4px;
521
+ border-radius: 5px;
522
+ }
523
+ .dsh-ac-viewer__back:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.08)); color: var(--dsw-alias-label-primary, #f9fafb); }
524
+ .dsh-ac-viewer__title {
525
+ flex: 1;
526
+ min-width: 0;
527
+ font-size: 12px;
528
+ font-weight: 600;
529
+ overflow: hidden;
530
+ text-overflow: ellipsis;
531
+ white-space: nowrap;
532
+ }
533
+ .dsh-ac__dialog-mask {
534
+ position: absolute;
535
+ inset: 0;
536
+ z-index: 30;
537
+ display: flex;
538
+ align-items: center;
539
+ justify-content: center;
540
+ padding: 12px;
541
+ background: color-mix(in srgb, var(--dsw-alias-bg-base, #151517) 55%, transparent);
542
+ backdrop-filter: blur(3px);
543
+ }
544
+ .dsh-ac__dialog {
545
+ width: min(420px, 100%);
546
+ max-height: 100%;
547
+ overflow: auto;
548
+ border: 1px solid var(--dsw-alias-border-l2, rgba(255,255,255,.12));
549
+ border-radius: 10px;
550
+ background: var(--dsw-alias-bg-layer-2, #2c2c2e);
551
+ box-shadow: var(--dsw-elevation-prominent, 0 18px 48px rgba(0,0,0,.4));
552
+ }
553
+ .dsh-ac__dialog-head {
554
+ display: flex;
555
+ align-items: center;
556
+ justify-content: space-between;
557
+ padding: 10px 12px;
558
+ border-bottom: 1px solid var(--dsw-alias-border-l1, rgba(255,255,255,.06));
559
+ font-size: 12px;
560
+ font-weight: 600;
561
+ }
562
+ .dsh-ac__dialog-body { display: flex; flex-direction: column; gap: 10px; padding: 12px; }
563
+ .dsh-ac__export-option {
564
+ display: flex;
565
+ flex-direction: column;
566
+ gap: 5px;
567
+ padding: 10px;
568
+ border: 1px solid var(--dsw-alias-border-l1, rgba(255,255,255,.06));
569
+ border-radius: 8px;
570
+ background: var(--dsw-alias-bg-layer-1, #232324);
571
+ }
572
+ .dsh-ac__export-title { font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-primary, #f9fafb); }
573
+ .dsh-ac__export-desc { margin: 0; font-size: 10px; line-height: 15px; color: var(--dsw-alias-label-secondary, #cfd3d6); }
574
+ .dsh-ac__export-notice {
575
+ padding: 6px 9px;
576
+ border-radius: 6px;
577
+ background: color-mix(in srgb, var(--dsw-alias-state-success-primary, #22c55e) 12%, transparent);
578
+ color: var(--dsw-alias-state-success-primary, #22c55e);
579
+ font-size: 10px;
580
+ }
581
+ `;
582
+ /** Cockpit catalogue plus viewer. */
583
+ function CockpitTab({ tab, scope, insertArtifactReference }) {
584
+ const meta = readMeta(tab.meta);
585
+ const [apps, setApps] = (0, react.useState)([]);
586
+ const [templates, setTemplates] = (0, react.useState)([]);
587
+ const [components, setComponents] = (0, react.useState)([]);
588
+ const [loading, setLoading] = (0, react.useState)(true);
589
+ const [error, setError] = (0, react.useState)(null);
590
+ const [templateError, setTemplateError] = (0, react.useState)(null);
591
+ const [componentError, setComponentError] = (0, react.useState)(null);
592
+ const [notice, setNotice] = (0, react.useState)("");
593
+ const [selected, setSelected] = (0, react.useState)(meta.cockpitId === void 0 ? null : {
594
+ kind: "app",
595
+ viewerId: meta.cockpitId,
596
+ name: meta.cockpitName ?? "驾驶舱"
597
+ });
598
+ const [reloadToken, setReloadToken] = (0, react.useState)(0);
599
+ const load = (0, react.useCallback)(async () => {
600
+ setLoading(true);
601
+ setError(null);
602
+ setTemplateError(null);
603
+ setComponentError(null);
604
+ const fetchList = (url) => fetch(url, { headers: { accept: "application/json" } }).then(async (response) => {
605
+ const payload = await response.json();
606
+ if (!response.ok) throw new Error(payload.error ?? "HTTP " + String(response.status));
607
+ return readItems(payload);
608
+ });
609
+ const [appsResult, templatesResult, componentsResult] = await Promise.allSettled([
610
+ fetch("/dsh-artifact-app/apps?pageSize=50", { headers: { accept: "application/json" } }).then(async (response) => {
611
+ const payload = await response.json();
612
+ if (!response.ok) throw new Error(payload.error ?? "HTTP " + String(response.status));
613
+ return payload.items ?? [];
614
+ }),
615
+ fetchList("/dsh-artifact-app/templates"),
616
+ fetchList("/dsh-artifact-app/templates?appType=COMPONENT")
617
+ ]);
618
+ if (appsResult.status === "fulfilled") setApps(appsResult.value);
619
+ else setError(appsResult.reason instanceof Error ? appsResult.reason.message : String(appsResult.reason));
620
+ if (templatesResult.status === "fulfilled") setTemplates(templatesResult.value);
621
+ else setTemplateError(templatesResult.reason instanceof Error ? templatesResult.reason.message : String(templatesResult.reason));
622
+ if (componentsResult.status === "fulfilled") setComponents(componentsResult.value);
623
+ else setComponentError(componentsResult.reason instanceof Error ? componentsResult.reason.message : String(componentsResult.reason));
624
+ setLoading(false);
625
+ }, []);
626
+ (0, react.useEffect)(() => {
627
+ load();
628
+ }, [load]);
629
+ (0, react.useEffect)(() => {
630
+ if (meta.cockpitId === void 0) return;
631
+ setSelected({
632
+ kind: "app",
633
+ viewerId: meta.cockpitId,
634
+ name: meta.cockpitName ?? "驾驶舱"
635
+ });
636
+ setReloadToken((value) => value + 1);
637
+ }, [
638
+ meta.cockpitId,
639
+ meta.cockpitName,
640
+ meta.nonce
641
+ ]);
642
+ (0, react.useEffect)(() => {
643
+ if (notice === "") return void 0;
644
+ const timer = window.setTimeout(() => setNotice(""), 2400);
645
+ return () => window.clearTimeout(timer);
646
+ }, [notice]);
647
+ const catalogs = (0, react.useMemo)(() => [...new Set([
648
+ ...apps.map((app) => app.catalog),
649
+ ...templates.map((template) => template.catalog),
650
+ ...components.map((component) => component.catalog)
651
+ ].filter((value) => typeof value === "string" && value !== ""))], [
652
+ apps,
653
+ templates,
654
+ components
655
+ ]);
656
+ const openApp = (app) => setSelected({
657
+ kind: "app",
658
+ viewerId: app.id,
659
+ name: app.name
660
+ });
661
+ const openTemplate = (template) => setSelected({
662
+ kind: "template",
663
+ viewerId: "template/" + template.id,
664
+ name: template.name
665
+ });
666
+ const openComponent = (component) => setSelected({
667
+ kind: "component",
668
+ viewerId: "template/" + component.id,
669
+ name: component.name
670
+ });
671
+ const addToConversation = (item) => {
672
+ if (insertArtifactReference === void 0) {
673
+ setNotice("当前会话不可用,无法插入引用");
674
+ return;
675
+ }
676
+ try {
677
+ insertArtifactReference(item, scope?.sessionId);
678
+ setNotice("已添加到对话");
679
+ } catch (cause) {
680
+ setNotice("插入引用失败:" + (cause instanceof Error ? cause.message : String(cause)));
681
+ }
682
+ };
683
+ /**
684
+ * 导出对话框:常规(zip,素材外链)与全量(单文件,内联 assets)。
685
+ * 两者都走后端 `GET /api/artifact-apps/:id/export?mode=…`,经插件网关带调用者 token 转发。
686
+ */
687
+ const [exportOpen, setExportOpen] = (0, react.useState)(false);
688
+ const [exporting, setExporting] = (0, react.useState)(null);
689
+ const [exportNotice, setExportNotice] = (0, react.useState)("");
690
+ const fetchToken = async () => {
691
+ const response = await fetch("/dsh-artifact-app/token", {
692
+ headers: { accept: "application/json" },
693
+ cache: "no-store"
694
+ });
695
+ const payload = await response.json();
696
+ if (!response.ok || typeof payload.token !== "string" || payload.token.trim() === "") throw new Error(payload.error ?? "读取 DataOps 登录凭证失败");
697
+ return payload.token;
698
+ };
699
+ const downloadExport = async (mode) => {
700
+ if (selected === null || selected.kind !== "app" || exporting !== null) return;
701
+ setExporting(mode);
702
+ setExportNotice("");
703
+ try {
704
+ const token = await fetchToken();
705
+ const url = "/dsh-artifact-app/gateway/api/artifact-apps/" + encodeURIComponent(selected.viewerId) + "/export?mode=" + mode;
706
+ const response = await fetch(url, { headers: { "x-dataops-token": token } });
707
+ if (!response.ok) throw new Error("导出失败(HTTP " + String(response.status) + ")");
708
+ const blob = await response.blob();
709
+ const objectUrl = URL.createObjectURL(blob);
710
+ const link = document.createElement("a");
711
+ link.href = objectUrl;
712
+ link.download = (selected.name || "cockpit") + (mode === "external" ? ".zip" : ".html");
713
+ document.body.appendChild(link);
714
+ link.click();
715
+ link.remove();
716
+ URL.revokeObjectURL(objectUrl);
717
+ setExportNotice("已开始下载");
718
+ } catch (cause) {
719
+ setExportNotice("导出失败:" + (cause instanceof Error ? cause.message : String(cause)));
720
+ } finally {
721
+ setExporting(null);
722
+ }
723
+ };
724
+ const copyToken = async () => {
725
+ try {
726
+ const token = await fetchToken();
727
+ try {
728
+ await navigator.clipboard.writeText(token);
729
+ } catch {
730
+ const area = document.createElement("textarea");
731
+ area.value = token;
732
+ document.body.appendChild(area);
733
+ area.select();
734
+ document.execCommand("copy");
735
+ area.remove();
736
+ }
737
+ setExportNotice("token 已复制");
738
+ } catch (cause) {
739
+ setExportNotice("复制失败:" + (cause instanceof Error ? cause.message : String(cause)));
740
+ }
741
+ };
742
+ if (selected !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
743
+ className: "dsh-ac",
744
+ children: [
745
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: CSS }),
746
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
747
+ className: "dsh-ac-viewer",
748
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
749
+ className: "dsh-ac-viewer__head",
750
+ children: [
751
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
752
+ type: "button",
753
+ className: "dsh-ac-viewer__back",
754
+ onClick: () => setSelected(null),
755
+ children: "← 列表"
756
+ }),
757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
758
+ className: `dsh-ac__badge ${selected.kind === "component" ? "is-component" : selected.kind === "template" ? "is-template" : "is-app"}`,
759
+ children: selected.kind === "component" ? "组件" : selected.kind === "template" ? "模板" : "应用"
760
+ }),
761
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
762
+ className: "dsh-ac-viewer__title",
763
+ children: selected.name
764
+ }),
765
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
766
+ type: "button",
767
+ className: "dsh-ac__icon",
768
+ title: "刷新",
769
+ "aria-label": "刷新",
770
+ onClick: () => setReloadToken((value) => value + 1),
771
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
772
+ width: "14",
773
+ height: "14",
774
+ viewBox: "0 0 24 24",
775
+ fill: "none",
776
+ stroke: "currentColor",
777
+ strokeWidth: "1.9",
778
+ strokeLinecap: "round",
779
+ strokeLinejoin: "round",
780
+ "aria-hidden": "true",
781
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M21 12a9 9 0 1 1-3-6.7" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M21 4v5h-5" })]
782
+ })
783
+ }),
784
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
785
+ type: "button",
786
+ className: "dsh-ac__icon",
787
+ title: "全屏查看",
788
+ "aria-label": "全屏查看",
789
+ onClick: () => {
790
+ const target = document.querySelector("[data-artifact-fullscreen-host]") ?? document.querySelector("iframe[data-artifact-viewer]")?.parentElement ?? null;
791
+ if (target === null) return;
792
+ if (document.fullscreenElement !== null) document.exitFullscreen();
793
+ else target.requestFullscreen?.().catch(() => void 0);
794
+ },
795
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
796
+ width: "14",
797
+ height: "14",
798
+ viewBox: "0 0 24 24",
799
+ fill: "none",
800
+ stroke: "currentColor",
801
+ strokeWidth: "1.9",
802
+ strokeLinecap: "round",
803
+ strokeLinejoin: "round",
804
+ "aria-hidden": "true",
805
+ children: [
806
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 9V4h5" }),
807
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M20 9V4h-5" }),
808
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 15v5h5" }),
809
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M20 15v5h-5" })
810
+ ]
811
+ })
812
+ }),
813
+ selected.kind === "app" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
814
+ type: "button",
815
+ className: "dsh-ac__icon",
816
+ title: "导出",
817
+ "aria-label": "导出",
818
+ onClick: () => {
819
+ setExportNotice("");
820
+ setExportOpen(true);
821
+ },
822
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
823
+ width: "14",
824
+ height: "14",
825
+ viewBox: "0 0 24 24",
826
+ fill: "none",
827
+ stroke: "currentColor",
828
+ strokeWidth: "1.9",
829
+ strokeLinecap: "round",
830
+ strokeLinejoin: "round",
831
+ "aria-hidden": "true",
832
+ children: [
833
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 3v12" }),
834
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M7 10l5 5 5-5" }),
835
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 21h16" })
836
+ ]
837
+ })
838
+ }) : null
839
+ ]
840
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
841
+ "data-artifact-fullscreen-host": true,
842
+ style: {
843
+ flex: 1,
844
+ minHeight: 0
845
+ },
846
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CockpitViewer, {
847
+ artifactId: selected.viewerId,
848
+ reloadToken
849
+ })
850
+ })]
851
+ }),
852
+ exportOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
853
+ className: "dsh-ac__dialog-mask",
854
+ "data-export-dialog": true,
855
+ role: "dialog",
856
+ "aria-modal": "true",
857
+ "aria-label": "导出驾驶舱",
858
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
859
+ className: "dsh-ac__dialog",
860
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
861
+ className: "dsh-ac__dialog-head",
862
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "导出驾驶舱" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
863
+ type: "button",
864
+ className: "dsh-ac__icon",
865
+ "aria-label": "关闭",
866
+ onClick: () => setExportOpen(false),
867
+ children: "×"
868
+ })]
869
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
870
+ className: "dsh-ac__dialog-body",
871
+ children: [
872
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
873
+ className: "dsh-ac__export-option",
874
+ children: [
875
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
876
+ className: "dsh-ac__export-title",
877
+ children: "常规导出"
878
+ }),
879
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
880
+ className: "dsh-ac__export-desc",
881
+ children: "zip 包:HTML 与 assets 分离,解压后需静态托管;HTML 通过相对路径引用素材。"
882
+ }),
883
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
884
+ type: "button",
885
+ className: "dsh-ac__btn is-primary",
886
+ "data-export-mode": "external",
887
+ disabled: exporting !== null,
888
+ onClick: () => void downloadExport("external"),
889
+ children: exporting === "external" ? "导出中…" : "下载 zip"
890
+ })
891
+ ]
892
+ }),
893
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
894
+ className: "dsh-ac__export-option",
895
+ children: [
896
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
897
+ className: "dsh-ac__export-title",
898
+ children: "全量导出(内联 assets)"
899
+ }),
900
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
901
+ className: "dsh-ac__export-desc",
902
+ children: "单文件 HTML:双击即可打开;取数时需要粘贴 token。"
903
+ }),
904
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
905
+ type: "button",
906
+ className: "dsh-ac__btn is-primary",
907
+ "data-export-mode": "inline",
908
+ disabled: exporting !== null,
909
+ onClick: () => void downloadExport("inline"),
910
+ children: exporting === "inline" ? "导出中…" : "下载 HTML"
911
+ })
912
+ ]
913
+ }),
914
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
915
+ className: "dsh-ac__export-option",
916
+ children: [
917
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
918
+ className: "dsh-ac__export-title",
919
+ children: "当前 token"
920
+ }),
921
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
922
+ className: "dsh-ac__export-desc",
923
+ children: "全量导出取数时使用。当前 JWT 约到 2027-09,实际以登录态为准。"
924
+ }),
925
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
926
+ type: "button",
927
+ className: "dsh-ac__btn",
928
+ "data-copy-token": true,
929
+ onClick: () => void copyToken(),
930
+ children: "复制当前 token"
931
+ })
932
+ ]
933
+ }),
934
+ exportNotice !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
935
+ className: "dsh-ac__export-notice",
936
+ role: "status",
937
+ children: exportNotice
938
+ }) : null
939
+ ]
940
+ })]
941
+ })
942
+ }) : null,
943
+ notice !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
944
+ className: "dsh-ac__toast",
945
+ role: "status",
946
+ children: notice
947
+ }) : null
948
+ ]
949
+ });
950
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
951
+ className: "dsh-ac",
952
+ children: [
953
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: CSS }),
954
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
955
+ className: "dsh-ac__head",
956
+ children: [
957
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
958
+ className: "dsh-ac__glyph",
959
+ "aria-hidden": "true",
960
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
961
+ width: "14",
962
+ height: "14",
963
+ viewBox: "0 0 24 24",
964
+ fill: "none",
965
+ stroke: "currentColor",
966
+ strokeWidth: "1.8",
967
+ strokeLinecap: "round",
968
+ strokeLinejoin: "round",
969
+ children: [
970
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 18a8 8 0 1 1 16 0" }),
971
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 18l4.2-5" }),
972
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
973
+ cx: "12",
974
+ cy: "18",
975
+ r: "1.4"
976
+ })
977
+ ]
978
+ })
979
+ }),
980
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
981
+ className: "dsh-ac__heading",
982
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
983
+ className: "dsh-ac__name",
984
+ children: "驾驶舱"
985
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
986
+ className: "dsh-ac__sub",
987
+ children: [
988
+ apps.length,
989
+ " 个应用 · ",
990
+ templates.length,
991
+ " 个模板 · ",
992
+ components.length,
993
+ " 个组件",
994
+ catalogs.length > 0 ? " · " + catalogs.join(" / ") : ""
995
+ ]
996
+ })]
997
+ }),
998
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
999
+ type: "button",
1000
+ className: "dsh-ac__icon",
1001
+ title: "刷新",
1002
+ "aria-label": "刷新",
1003
+ onClick: () => void load(),
1004
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1005
+ width: "14",
1006
+ height: "14",
1007
+ viewBox: "0 0 24 24",
1008
+ fill: "none",
1009
+ stroke: "currentColor",
1010
+ strokeWidth: "1.9",
1011
+ strokeLinecap: "round",
1012
+ strokeLinejoin: "round",
1013
+ "aria-hidden": "true",
1014
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M21 12a9 9 0 1 1-3-6.7" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M21 4v5h-5" })]
1015
+ })
1016
+ })
1017
+ ]
1018
+ }),
1019
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1020
+ className: "dsh-ac__body",
1021
+ children: [
1022
+ loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1023
+ className: "dsh-ac__state",
1024
+ children: "正在加载驾驶舱资产…"
1025
+ }) : null,
1026
+ error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1027
+ className: "dsh-ac__error",
1028
+ role: "alert",
1029
+ children: error
1030
+ }) : null,
1031
+ !loading && error === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1032
+ className: "dsh-ac__section",
1033
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1034
+ className: "dsh-ac__section-head",
1035
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "应用" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1036
+ className: "dsh-ac__count",
1037
+ children: apps.length
1038
+ })]
1039
+ }), apps.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1040
+ className: "dsh-ac__state",
1041
+ children: "暂无驾驶舱应用"
1042
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1043
+ className: "dsh-ac__list",
1044
+ children: apps.map((app) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1045
+ className: "dsh-ac__card",
1046
+ "data-cockpit-id": app.id,
1047
+ tabIndex: 0,
1048
+ onClick: () => openApp(app),
1049
+ onKeyDown: (event) => {
1050
+ if (event.key === "Enter" || event.key === " ") {
1051
+ event.preventDefault();
1052
+ openApp(app);
1053
+ }
1054
+ },
1055
+ children: [
1056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1057
+ className: "dsh-ac__card-top",
1058
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1059
+ className: "dsh-ac__badge is-app",
1060
+ children: "应用"
1061
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1062
+ className: `dsh-ac__status ${app.status === "PUBLISHED" ? "is-published" : "is-draft"}`,
1063
+ children: app.status === "PUBLISHED" ? "已发布" : "草稿"
1064
+ })]
1065
+ }),
1066
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1067
+ className: "dsh-ac__card-title",
1068
+ title: app.name,
1069
+ children: app.name
1070
+ }),
1071
+ app.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1072
+ className: "dsh-ac__card-desc",
1073
+ children: app.description
1074
+ }) : null,
1075
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1076
+ className: "dsh-ac__meta",
1077
+ children: [
1078
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: app.catalog ?? "未分类" }),
1079
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dot" }),
1080
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: versionLabel(app) })
1081
+ ]
1082
+ }),
1083
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1084
+ className: "dsh-ac__actions",
1085
+ onClick: (event) => event.stopPropagation(),
1086
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1087
+ type: "button",
1088
+ className: "dsh-ac__btn is-primary",
1089
+ "data-action": "view",
1090
+ onClick: () => openApp(app),
1091
+ children: "查看"
1092
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1093
+ type: "button",
1094
+ className: "dsh-ac__btn",
1095
+ "data-action": "insert",
1096
+ onClick: () => addToConversation({
1097
+ kind: "app",
1098
+ id: app.id,
1099
+ name: app.name,
1100
+ catalog: app.catalog
1101
+ }),
1102
+ children: "添加到对话"
1103
+ })]
1104
+ })
1105
+ ]
1106
+ }, app.id))
1107
+ })]
1108
+ }) : null,
1109
+ !loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1110
+ className: "dsh-ac__section",
1111
+ children: [
1112
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1113
+ className: "dsh-ac__section-head",
1114
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "模板" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1115
+ className: "dsh-ac__count",
1116
+ children: templates.length
1117
+ })]
1118
+ }),
1119
+ templateError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1120
+ className: "dsh-ac__error",
1121
+ role: "alert",
1122
+ children: ["模板加载失败:", templateError]
1123
+ }) : null,
1124
+ templateError === null && templates.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1125
+ className: "dsh-ac__state",
1126
+ children: "暂无驾驶舱模板"
1127
+ }) : null,
1128
+ templates.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1129
+ className: "dsh-ac__list",
1130
+ children: templates.map((template) => {
1131
+ const slots = template.contract?.slots ?? [];
1132
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1133
+ className: "dsh-ac__card",
1134
+ "data-template-id": template.id,
1135
+ tabIndex: 0,
1136
+ onClick: () => openTemplate(template),
1137
+ onKeyDown: (event) => {
1138
+ if (event.key === "Enter" || event.key === " ") {
1139
+ event.preventDefault();
1140
+ openTemplate(template);
1141
+ }
1142
+ },
1143
+ children: [
1144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1145
+ className: "dsh-ac__card-top",
1146
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1147
+ className: "dsh-ac__badge is-template",
1148
+ children: "模板"
1149
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1150
+ className: "dsh-ac__status",
1151
+ children: versionLabel(template)
1152
+ })]
1153
+ }),
1154
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1155
+ className: "dsh-ac__card-title",
1156
+ title: template.name,
1157
+ children: template.name
1158
+ }),
1159
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1160
+ className: "dsh-ac__card-desc",
1161
+ children: template.description || template.contract?.question || "驾驶舱模板,可选择后填写数据槽位。"
1162
+ }),
1163
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1164
+ className: "dsh-ac__meta",
1165
+ children: [
1166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: template.catalog ?? "未分类" }),
1167
+ slots.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dot" }) : null,
1168
+ slots.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [slots.length, " 个槽位"] }) : null
1169
+ ]
1170
+ }),
1171
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1172
+ className: "dsh-ac__actions",
1173
+ onClick: (event) => event.stopPropagation(),
1174
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1175
+ type: "button",
1176
+ className: "dsh-ac__btn is-primary",
1177
+ "data-action": "view",
1178
+ onClick: () => openTemplate(template),
1179
+ children: "查看"
1180
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1181
+ type: "button",
1182
+ className: "dsh-ac__btn",
1183
+ "data-action": "insert",
1184
+ onClick: () => addToConversation({
1185
+ kind: "template",
1186
+ id: template.id,
1187
+ name: template.name,
1188
+ catalog: template.catalog
1189
+ }),
1190
+ children: "添加到对话"
1191
+ })]
1192
+ })
1193
+ ]
1194
+ }, template.id);
1195
+ })
1196
+ }) : null
1197
+ ]
1198
+ }) : null,
1199
+ !loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1200
+ className: "dsh-ac__section",
1201
+ children: [
1202
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1203
+ className: "dsh-ac__section-head",
1204
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "组件" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1205
+ className: "dsh-ac__count",
1206
+ children: components.length
1207
+ })]
1208
+ }),
1209
+ componentError !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1210
+ className: "dsh-ac__error",
1211
+ role: "alert",
1212
+ children: ["组件加载失败:", componentError]
1213
+ }) : null,
1214
+ componentError === null && components.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1215
+ className: "dsh-ac__state",
1216
+ children: "暂无组件参考"
1217
+ }) : null,
1218
+ components.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1219
+ className: "dsh-ac__list",
1220
+ children: components.map((component) => {
1221
+ const tags = Array.isArray(component.contract?.tags) ? component.contract.tags : [];
1222
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
1223
+ className: "dsh-ac__card",
1224
+ "data-component-id": component.id,
1225
+ tabIndex: 0,
1226
+ onClick: () => openComponent(component),
1227
+ onKeyDown: (event) => {
1228
+ if (event.key === "Enter" || event.key === " ") {
1229
+ event.preventDefault();
1230
+ openComponent(component);
1231
+ }
1232
+ },
1233
+ children: [
1234
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1235
+ className: "dsh-ac__card-top",
1236
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1237
+ className: "dsh-ac__badge is-component",
1238
+ children: "组件"
1239
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1240
+ className: "dsh-ac__status",
1241
+ children: versionLabel(component)
1242
+ })]
1243
+ }),
1244
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1245
+ className: "dsh-ac__card-title",
1246
+ title: component.name,
1247
+ children: component.name
1248
+ }),
1249
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1250
+ className: "dsh-ac__card-desc",
1251
+ children: component.description || "可预览、供 AI 参考的组件实现。"
1252
+ }),
1253
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1254
+ className: "dsh-ac__meta",
1255
+ children: [
1256
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: component.catalog ?? "未分类" }),
1257
+ tags.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dot" }) : null,
1258
+ tags.slice(0, 4).map((tag) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tag }, tag))
1259
+ ]
1260
+ }),
1261
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1262
+ className: "dsh-ac__actions",
1263
+ onClick: (event) => event.stopPropagation(),
1264
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1265
+ type: "button",
1266
+ className: "dsh-ac__btn is-primary",
1267
+ "data-action": "view",
1268
+ onClick: () => openComponent(component),
1269
+ children: "查看"
1270
+ })
1271
+ })
1272
+ ]
1273
+ }, component.id);
1274
+ })
1275
+ }) : null
1276
+ ]
1277
+ }) : null
1278
+ ]
1279
+ }),
1280
+ notice !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1281
+ className: "dsh-ac__toast",
1282
+ role: "status",
1283
+ children: notice
1284
+ }) : null
1285
+ ]
1286
+ });
1287
+ }
1288
+ //#endregion
1289
+ //#region src/client/ArtifactFileViewer.tsx
1290
+ /**
1291
+ * HTML file viewer for cockpit files.
1292
+ *
1293
+ * The descriptor only matches `/artifact-apps/**` HTML carrying the
1294
+ * `artifact-app` meta marker. Opening such a file creates or updates one draft
1295
+ * through the host and renders it exactly like a published cockpit, so the file
1296
+ * on disk is the draft.
1297
+ *
1298
+ * @module client/ArtifactFileViewer
1299
+ */
1300
+ /** Marker the file must carry for this viewer to claim it. */
1301
+ const ARTIFACT_FILE_MARKER = /<meta[^>]+name=["']artifact-app["'][^>]*>/iu;
1302
+ /**
1303
+ * Whether a file is a cockpit document.
1304
+ * @param path - workspace path.
1305
+ * @param head - the first bytes the viewer sniffed.
1306
+ */
1307
+ const isArtifactFile = (path, head) => {
1308
+ if (!/\/artifact-apps\/[^/]+\.html?$/iu.test(path)) return false;
1309
+ const text = typeof head === "string" ? head : new TextDecoder().decode(head);
1310
+ return ARTIFACT_FILE_MARKER.test(text);
1311
+ };
1312
+ /** Create/update the preview draft, then render it. */
1313
+ function ArtifactFileViewer({ path, content, largeFile }) {
1314
+ const [artifactId, setArtifactId] = (0, react.useState)(null);
1315
+ const [error, setError] = (0, react.useState)(null);
1316
+ const [busy, setBusy] = (0, react.useState)(false);
1317
+ const [attempt, setAttempt] = (0, react.useState)(0);
1318
+ (0, react.useEffect)(() => {
1319
+ if (content === void 0) return void 0;
1320
+ let alive = true;
1321
+ setBusy(true);
1322
+ setError(null);
1323
+ (async () => {
1324
+ try {
1325
+ const response = await fetch("/dsh-artifact-app/preview", {
1326
+ method: "POST",
1327
+ headers: {
1328
+ "content-type": "application/json",
1329
+ accept: "application/json"
1330
+ },
1331
+ body: JSON.stringify({
1332
+ path,
1333
+ html: content
1334
+ })
1335
+ });
1336
+ const payload = await response.json();
1337
+ if (!response.ok || typeof payload.artifactId !== "string") throw new Error(payload.error ?? "HTTP " + String(response.status));
1338
+ if (alive) setArtifactId(payload.artifactId);
1339
+ } catch (cause) {
1340
+ if (alive) setError(cause instanceof Error ? cause.message : String(cause));
1341
+ } finally {
1342
+ if (alive) setBusy(false);
1343
+ }
1344
+ })();
1345
+ return () => {
1346
+ alive = false;
1347
+ };
1348
+ }, [
1349
+ path,
1350
+ content,
1351
+ attempt
1352
+ ]);
1353
+ if (largeFile !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1354
+ style: {
1355
+ padding: 18,
1356
+ fontSize: 12,
1357
+ color: "var(--dsw-alias-label-secondary, #6b7280)"
1358
+ },
1359
+ children: [
1360
+ "文件过大(",
1361
+ Math.round(largeFile.size / 1024),
1362
+ " KB),请缩小后再预览驾驶舱。"
1363
+ ]
1364
+ });
1365
+ if (error !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1366
+ role: "alert",
1367
+ style: {
1368
+ padding: 12,
1369
+ margin: 12,
1370
+ borderRadius: 8,
1371
+ background: "#fef2f2",
1372
+ color: "#b91c1c",
1373
+ fontSize: 12,
1374
+ display: "grid",
1375
+ gap: 8
1376
+ },
1377
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1378
+ style: { whiteSpace: "pre-wrap" },
1379
+ children: error
1380
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1381
+ type: "button",
1382
+ onClick: () => setAttempt((value) => value + 1),
1383
+ style: {
1384
+ justifySelf: "start",
1385
+ border: "1px solid #fca5a5",
1386
+ background: "transparent",
1387
+ color: "#b91c1c",
1388
+ borderRadius: 6,
1389
+ padding: "3px 10px",
1390
+ cursor: "pointer",
1391
+ fontSize: 11
1392
+ },
1393
+ children: "重试"
1394
+ })]
1395
+ });
1396
+ if (busy || artifactId === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1397
+ style: {
1398
+ padding: 18,
1399
+ fontSize: 12,
1400
+ color: "var(--dsw-alias-label-secondary, #6b7280)"
1401
+ },
1402
+ children: "正在生成驾驶舱预览…"
1403
+ });
1404
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CockpitViewer, { artifactId });
1405
+ }
1406
+ //#endregion
1407
+ //#region src/client/index.tsx
1408
+ /**
1409
+ * Client half of the artifact-app plugin.
1410
+ *
1411
+ * Registers the `dataops-artifact-apps` right-sidebar tab, the cockpit HTML file
1412
+ * viewer, and the SSE bridge that lets `cockpit_open` drive the sidebar. All three
1413
+ * are optional when a profile has no Better Sidebar: the host routes and tools keep
1414
+ * working without a browser surface.
1415
+ *
1416
+ * @module @sparkelf/dsh-artifact-app/client
1417
+ */
1418
+ /** Client services this plugin reads; `slots` owns the always-mounted bridge seat. */
1419
+ const inject = ["slots"];
1420
+ /** Reference source id shared by the chip codec and the inserted reference. */
1421
+ const REFERENCE_SOURCE = "dataops-artifact-app";
1422
+ /** Tab id shared by registration, the bridge and the tools. */
1423
+ const ARTIFACT_TAB_TYPE = "dataops-artifact-apps";
1424
+ /** File viewer id. */
1425
+ const ARTIFACT_VIEWER_ID = "artifact-app:cockpit";
1426
+ /** Invisible bridge: SSE navigation events become Better Sidebar calls. */
1427
+ function OpenCockpitBridge({ service, scope }) {
1428
+ (0, react.useEffect)(() => {
1429
+ if (typeof EventSource !== "function") return void 0;
1430
+ const source = new EventSource("/dsh-artifact-app/open/events");
1431
+ const open = (event) => {
1432
+ let parsed;
1433
+ try {
1434
+ parsed = JSON.parse(String(event.data));
1435
+ } catch {
1436
+ return;
1437
+ }
1438
+ if (parsed.type !== "open_cockpit" || typeof parsed.cockpitId !== "string") return;
1439
+ const meta = {
1440
+ cockpitId: parsed.cockpitId,
1441
+ cockpitName: parsed.cockpitName,
1442
+ nonce: parsed.nonce,
1443
+ mode: parsed.mode
1444
+ };
1445
+ service.openTab?.({
1446
+ type: ARTIFACT_TAB_TYPE,
1447
+ id: ARTIFACT_TAB_TYPE,
1448
+ title: "驾驶舱",
1449
+ meta
1450
+ }, scope);
1451
+ service.updateTab?.(ARTIFACT_TAB_TYPE, {
1452
+ title: "驾驶舱",
1453
+ meta
1454
+ }, scope?.sessionId);
1455
+ };
1456
+ const updated = (event) => {
1457
+ let parsed;
1458
+ try {
1459
+ parsed = JSON.parse(String(event.data));
1460
+ } catch {
1461
+ return;
1462
+ }
1463
+ if (parsed.type !== "cockpit_updated" || typeof parsed.cockpitId !== "string") return;
1464
+ service.updateTab?.(ARTIFACT_TAB_TYPE, {
1465
+ title: "驾驶舱",
1466
+ meta: {
1467
+ cockpitId: parsed.cockpitId,
1468
+ nonce: parsed.nonce
1469
+ }
1470
+ }, scope?.sessionId);
1471
+ };
1472
+ source.addEventListener("dataops-open-cockpit", open);
1473
+ source.addEventListener("dataops-cockpit-updated", updated);
1474
+ return () => {
1475
+ source.removeEventListener("dataops-open-cockpit", open);
1476
+ source.removeEventListener("dataops-cockpit-updated", updated);
1477
+ source.close();
1478
+ };
1479
+ }, [service, scope]);
1480
+ return null;
1481
+ }
1482
+ /**
1483
+ * Register the tab, the file viewer and the bridge.
1484
+ * @param ctx - DSH client context.
1485
+ */
1486
+ function apply(ctx) {
1487
+ try {
1488
+ ctx.effect(() => {
1489
+ let disposeTab;
1490
+ let disposeViewer;
1491
+ let disposeBridge;
1492
+ let betterSidebarService;
1493
+ const inject = ctx.inject;
1494
+ inject?.([
1495
+ "betterSidebar",
1496
+ "conversation",
1497
+ "sessions",
1498
+ "inputTriggers"
1499
+ ], (injected) => {
1500
+ const service = injected.betterSidebar;
1501
+ if (service?.registerTab === void 0) return;
1502
+ betterSidebarService = service;
1503
+ if (injected.inputTriggers?.registerSource !== void 0) {
1504
+ const source = {
1505
+ trigger: "@",
1506
+ name: REFERENCE_SOURCE,
1507
+ showGroupTitle: false,
1508
+ candidates: async () => [],
1509
+ codec: {
1510
+ clipboardText: (ref) => {
1511
+ try {
1512
+ return "@" + String(JSON.parse(ref).name ?? "驾驶舱");
1513
+ } catch {
1514
+ return "@驾驶舱";
1515
+ }
1516
+ },
1517
+ serialize: async (ref) => {
1518
+ try {
1519
+ const parsed = JSON.parse(ref);
1520
+ return "引用驾驶舱:\n\n```cockpit-ref\n" + JSON.stringify({
1521
+ kind: REFERENCE_SOURCE,
1522
+ assetKind: parsed.assetKind === "template" ? "template" : "app",
1523
+ id: parsed.id,
1524
+ name: parsed.name,
1525
+ catalog: parsed.catalog ?? null
1526
+ }, null, 2) + "\n```\n";
1527
+ } catch {
1528
+ return ref;
1529
+ }
1530
+ }
1531
+ }
1532
+ };
1533
+ try {
1534
+ injected.inputTriggers.registerSource(source);
1535
+ } catch {}
1536
+ }
1537
+ const insertArtifactReference = (item, sessionId) => {
1538
+ if (sessionId === void 0) throw new Error("no active session");
1539
+ const input = injected.conversation?.input?.for?.(injected.sessions?.scope?.(sessionId));
1540
+ if (input?.insertReference === void 0 || input.state?.getSnapshot === void 0) throw new Error("composer is unavailable");
1541
+ const state = input.state.getSnapshot();
1542
+ const span = {
1543
+ start: state.draft.length,
1544
+ end: state.draft.length,
1545
+ draftRev: state.draftRev
1546
+ };
1547
+ if (!input.insertReference({
1548
+ source: REFERENCE_SOURCE,
1549
+ ref: JSON.stringify({
1550
+ kind: REFERENCE_SOURCE,
1551
+ assetKind: item.kind,
1552
+ id: item.id,
1553
+ name: item.name,
1554
+ catalog: item.catalog ?? null
1555
+ }),
1556
+ label: item.name,
1557
+ appearance: "resource",
1558
+ clipboardText: "@" + item.name
1559
+ }, span)) throw new Error("输入框正忙,请稍后再试");
1560
+ };
1561
+ disposeTab = service.registerTab({
1562
+ id: ARTIFACT_TAB_TYPE,
1563
+ title: "驾驶舱",
1564
+ icon: ({ size = 14 }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1565
+ width: size >= 22 ? 22 : size,
1566
+ height: size >= 22 ? 22 : size,
1567
+ viewBox: "0 0 24 24",
1568
+ fill: "none",
1569
+ stroke: "currentColor",
1570
+ strokeWidth: "1.8",
1571
+ strokeLinecap: "round",
1572
+ strokeLinejoin: "round",
1573
+ "aria-hidden": "true",
1574
+ children: [
1575
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 18a8 8 0 1 1 16 0" }),
1576
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 18l4.2-5" }),
1577
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1578
+ cx: "12",
1579
+ cy: "18",
1580
+ r: "1.4"
1581
+ })
1582
+ ]
1583
+ }),
1584
+ description: "DataOps 驾驶舱资产、预览与发布",
1585
+ order: 62,
1586
+ single: true,
1587
+ component: ((props) => CockpitTab({
1588
+ ...props,
1589
+ insertArtifactReference
1590
+ }))
1591
+ });
1592
+ disposeViewer = service.registerFileViewer?.({
1593
+ id: ARTIFACT_VIEWER_ID,
1594
+ title: "驾驶舱预览",
1595
+ exts: [],
1596
+ priority: 60,
1597
+ fetchStrategy: "fsRead",
1598
+ detect: (path, head) => isArtifactFile(path, head),
1599
+ component: ((props) => ArtifactFileViewer(props))
1600
+ });
1601
+ });
1602
+ const slots = ctx.slots;
1603
+ disposeBridge = slots.inject("sidebar.footer.action", () => slots.register({
1604
+ name: "sidebar.footer.action",
1605
+ id: "artifact-app-bridge",
1606
+ order: 96
1607
+ }, ((props) => betterSidebarService === void 0 ? null : OpenCockpitBridge({
1608
+ service: betterSidebarService,
1609
+ scope: props?.scope
1610
+ }))));
1611
+ return () => {
1612
+ disposeBridge?.();
1613
+ disposeBridge = void 0;
1614
+ disposeViewer?.();
1615
+ disposeViewer = void 0;
1616
+ disposeTab?.();
1617
+ disposeTab = void 0;
1618
+ };
1619
+ }, "artifact-app: sidebar tab, file viewer and open bridge");
1620
+ } catch (error) {
1621
+ console.error("[artifact-app] apply failed", error);
1622
+ throw error;
1623
+ }
1624
+ }
1625
+ //#endregion
1626
+ exports.ARTIFACT_TAB_TYPE = ARTIFACT_TAB_TYPE;
1627
+ exports.ARTIFACT_VIEWER_ID = ARTIFACT_VIEWER_ID;
1628
+ exports.apply = apply;
1629
+ exports.inject = inject;
1630
+ return module.exports;
1631
+ }
1632
+ });
1633
+
1634
+ //# sourceMappingURL=client.js.map