@sparkelf/dsh-artifact-app 0.1.0 → 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 CHANGED
@@ -16,6 +16,18 @@ window.__ModuleLoader__.load({
16
16
  *
17
17
  * @module client/CockpitViewer
18
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
+ };
19
31
  const frameStyle = {
20
32
  width: "100%",
21
33
  height: "100%",
@@ -34,6 +46,9 @@ window.__ModuleLoader__.load({
34
46
  const frameRef = (0, react.useRef)(null);
35
47
  /** iframe 加载完成后重发一次引导配置:`hello` 可能在监听器挂上之前就发出去了。 */
36
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);
37
52
  const [config, setConfig] = (0, react.useState)(null);
38
53
  const [token, setToken] = (0, react.useState)(null);
39
54
  const [error, setError] = (0, react.useState)(null);
@@ -45,7 +60,7 @@ window.__ModuleLoader__.load({
45
60
  const configPayload = await configResponse.json();
46
61
  const tokenPayload = await tokenResponse.json();
47
62
  if (!configResponse.ok) throw new Error(configPayload.error ?? "读取驾驶舱配置失败");
48
- if (!tokenResponse.ok || typeof tokenPayload.token !== "string") throw new Error(tokenPayload.error ?? "读取 DataOps 登录凭证失败");
63
+ if (!tokenResponse.ok || typeof tokenPayload.token !== "string" || tokenPayload.token.trim() === "") throw new Error(tokenPayload.error ?? "读取 DataOps 登录凭证失败");
49
64
  if (alive) {
50
65
  setConfig(configPayload);
51
66
  setToken(tokenPayload.token);
@@ -60,14 +75,16 @@ window.__ModuleLoader__.load({
60
75
  }, []);
61
76
  (0, react.useEffect)(() => {
62
77
  if (config === null || token === null) return void 0;
63
- /** 把引导配置推给 iframe;`hello` 与 iframe 的 load 事件都会触发它。 */
64
- const sendConfig = () => {
78
+ /** 把引导配置推给 iframe;`hello`、`ready` 与 iframe 的 load 事件都会触发它。 */
79
+ const sendConfig = (tokenOverride) => {
80
+ const effectiveToken = tokenOverride ?? token;
81
+ if (effectiveToken === null || effectiveToken === "") return;
65
82
  frameRef.current?.contentWindow?.postMessage({
66
83
  source: "dataops-artifact-host",
67
84
  type: "config",
68
85
  payload: {
69
86
  artifactId,
70
- token,
87
+ token: effectiveToken,
71
88
  gatewayBaseUrl: config.gatewayBaseUrl,
72
89
  refreshIntervalSeconds: 300,
73
90
  keepConnection: false,
@@ -76,9 +93,51 @@ window.__ModuleLoader__.load({
76
93
  }
77
94
  }, "*");
78
95
  };
79
- const onMessage = (event) => {
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) => {
80
131
  const frame = frameRef.current;
81
- if (frame === null || event.source !== frame.contentWindow) return;
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;
82
141
  const data = event.data;
83
142
  if (data === null || typeof data !== "object" || data.source !== "dataops-artifact-app") return;
84
143
  if (data.type === "request-access") {
@@ -86,12 +145,34 @@ window.__ModuleLoader__.load({
86
145
  window.open(target, "_blank", "noopener,noreferrer");
87
146
  return;
88
147
  }
89
- if (data.type !== "hello" && data.type !== "ready" && data.type !== "token:expired") return;
90
- sendConfig();
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
+ }
91
162
  };
92
163
  resendRef.current = sendConfig;
93
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));
94
174
  return () => {
175
+ retryTimers.forEach((timer) => window.clearTimeout(timer));
95
176
  resendRef.current = null;
96
177
  window.removeEventListener("message", onMessage);
97
178
  };
@@ -109,7 +190,7 @@ window.__ModuleLoader__.load({
109
190
  },
110
191
  children: error
111
192
  });
112
- if (config === null || token === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
193
+ if (config === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
113
194
  style: {
114
195
  padding: 18,
115
196
  color: "var(--dsw-alias-label-secondary, #6b7280)",
@@ -117,7 +198,15 @@ window.__ModuleLoader__.load({
117
198
  },
118
199
  children: "正在准备驾驶舱…"
119
200
  });
120
- const src = config.renderBaseUrl.replace(/\/+$/u, "") + "/" + encodeURIComponent(artifactId) + (reloadToken === void 0 ? "" : "?r=" + String(reloadToken));
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));
121
210
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
122
211
  ref: frameRef,
123
212
  title: "驾驶舱",
@@ -134,9 +223,9 @@ window.__ModuleLoader__.load({
134
223
  /**
135
224
  * The `dataops-artifact-apps` right-sidebar tab.
136
225
  *
137
- * One pane holds the cockpit list and, once a card is opened, the sandboxed
138
- * viewer. The list is proxied by the host so the browser holds no DataOps token
139
- * for reads.
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.
140
229
  *
141
230
  * @module client/CockpitTab
142
231
  */
@@ -149,47 +238,390 @@ window.__ModuleLoader__.load({
149
238
  nonce: typeof record.nonce === "string" ? record.nonce : void 0
150
239
  };
151
240
  };
152
- const cardStyle = {
153
- display: "grid",
154
- gap: 4,
155
- width: "100%",
156
- minWidth: 0,
157
- textAlign: "left",
158
- padding: "10px 12px",
159
- borderRadius: 10,
160
- border: "1px solid var(--dsw-alias-border-l1, #e5e7eb)",
161
- background: "var(--dsw-alias-bg-layer-1, #ffffff)",
162
- color: "inherit",
163
- cursor: "pointer"
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 [];
164
245
  };
165
- const muted = {
166
- fontSize: 10,
167
- color: "var(--dsw-alias-label-secondary, #6b7280)"
246
+ const versionLabel = (row) => {
247
+ const version = row.publishedVersion || row.currentVersion || 1;
248
+ return "v" + String(version);
168
249
  };
169
- /** Cockpit list plus viewer. */
170
- function CockpitTab({ tab }) {
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 }) {
171
584
  const meta = readMeta(tab.meta);
172
585
  const [apps, setApps] = (0, react.useState)([]);
586
+ const [templates, setTemplates] = (0, react.useState)([]);
587
+ const [components, setComponents] = (0, react.useState)([]);
173
588
  const [loading, setLoading] = (0, react.useState)(true);
174
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)("");
175
593
  const [selected, setSelected] = (0, react.useState)(meta.cockpitId === void 0 ? null : {
176
- id: meta.cockpitId,
594
+ kind: "app",
595
+ viewerId: meta.cockpitId,
177
596
  name: meta.cockpitName ?? "驾驶舱"
178
597
  });
179
598
  const [reloadToken, setReloadToken] = (0, react.useState)(0);
180
599
  const load = (0, react.useCallback)(async () => {
181
600
  setLoading(true);
182
601
  setError(null);
183
- try {
184
- const response = await fetch("/dsh-artifact-app/apps?pageSize=50", { headers: { accept: "application/json" } });
602
+ setTemplateError(null);
603
+ setComponentError(null);
604
+ const fetchList = (url) => fetch(url, { headers: { accept: "application/json" } }).then(async (response) => {
185
605
  const payload = await response.json();
186
606
  if (!response.ok) throw new Error(payload.error ?? "HTTP " + String(response.status));
187
- setApps(payload.items ?? []);
188
- } catch (cause) {
189
- setError(cause instanceof Error ? cause.message : String(cause));
190
- } finally {
191
- setLoading(false);
192
- }
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);
193
625
  }, []);
194
626
  (0, react.useEffect)(() => {
195
627
  load();
@@ -197,7 +629,8 @@ window.__ModuleLoader__.load({
197
629
  (0, react.useEffect)(() => {
198
630
  if (meta.cockpitId === void 0) return;
199
631
  setSelected({
200
- id: meta.cockpitId,
632
+ kind: "app",
633
+ viewerId: meta.cockpitId,
201
634
  name: meta.cockpitName ?? "驾驶舱"
202
635
  });
203
636
  setReloadToken((value) => value + 1);
@@ -206,341 +639,650 @@ window.__ModuleLoader__.load({
206
639
  meta.cockpitName,
207
640
  meta.nonce
208
641
  ]);
209
- const catalogs = (0, react.useMemo)(() => [...new Set(apps.map((app) => app.catalog).filter((value) => typeof value === "string" && value !== ""))], [apps]);
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
+ };
210
683
  /**
211
- * 导出一份可脱离 DSH 单独分发的 HTML:服务端返回的 HTML 已烧死 appId 与网关地址,
212
- * 这里再把运行时与 ECharts 内联进去,产物双击即可打开(地址栏加 #token=… 取数)。
684
+ * 导出对话框:常规(zip,素材外链)与全量(单文件,内联 assets)。
685
+ * 两者都走后端 `GET /api/artifact-apps/:id/export?mode=…`,经插件网关带调用者 token 转发。
213
686
  */
214
- const [exporting, setExporting] = (0, react.useState)(false);
215
- const exportHtml = async () => {
216
- if (selected === null || exporting) return;
217
- setExporting(true);
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("");
218
703
  try {
219
- const base = "/dsh-artifact-app/gateway/api/artifact-apps";
220
- const [htmlResponse, runtimeResponse, echartsResponse] = await Promise.all([
221
- fetch(base.replace(/\/+$/u, "") + "/" + encodeURIComponent(selected.id) + "/html", { headers: { accept: "application/json" } }),
222
- fetch(base.replace(/\/+$/u, "") + "/runtime.js"),
223
- fetch(base.replace(/\/+$/u, "") + "/echarts.js")
224
- ]);
225
- if (!htmlResponse.ok) throw new Error("读取驾驶舱 HTML 失败");
226
- const payload = await htmlResponse.json();
227
- const runtime = runtimeResponse.ok ? await runtimeResponse.text() : "";
228
- const echarts = echartsResponse.ok ? await echartsResponse.text() : "";
229
- const inline = (runtime === "" ? "" : "<script>" + runtime + "<\/script>") + (echarts === "" ? "" : "<script>" + echarts + "<\/script>");
230
- const html = (payload.html ?? "").replace("</body>", inline + "</body>");
231
- const blob = new Blob([html], { type: "text/html;charset=utf-8" });
232
- const url = URL.createObjectURL(blob);
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);
233
710
  const link = document.createElement("a");
234
- link.href = url;
235
- link.download = (selected.name || "cockpit") + ".html";
711
+ link.href = objectUrl;
712
+ link.download = (selected.name || "cockpit") + (mode === "external" ? ".zip" : ".html");
236
713
  document.body.appendChild(link);
237
714
  link.click();
238
715
  link.remove();
239
- URL.revokeObjectURL(url);
716
+ URL.revokeObjectURL(objectUrl);
717
+ setExportNotice("已开始下载");
240
718
  } catch (cause) {
241
- setError(cause instanceof Error ? cause.message : String(cause));
719
+ setExportNotice("导出失败:" + (cause instanceof Error ? cause.message : String(cause)));
242
720
  } finally {
243
- setExporting(false);
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)));
244
740
  }
245
741
  };
246
742
  if (selected !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
247
- style: {
248
- display: "flex",
249
- flexDirection: "column",
250
- height: "100%",
251
- minHeight: 0
252
- },
253
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
254
- style: {
255
- display: "flex",
256
- alignItems: "center",
257
- gap: 8,
258
- padding: "8px 10px",
259
- borderBottom: "1px solid var(--dsw-alias-border-l1, #e5e7eb)"
260
- },
261
- children: [
262
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
263
- type: "button",
264
- onClick: () => setSelected(null),
265
- style: {
266
- border: "none",
267
- background: "transparent",
268
- color: "inherit",
269
- cursor: "pointer",
270
- fontSize: 12,
271
- padding: 0
272
- },
273
- children: "← 列表"
274
- }),
275
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("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,
276
842
  style: {
277
843
  flex: 1,
278
- minWidth: 0,
279
- fontSize: 12,
280
- fontWeight: 600,
281
- overflow: "hidden",
282
- textOverflow: "ellipsis",
283
- whiteSpace: "nowrap"
844
+ minHeight: 0
284
845
  },
285
- children: selected.name
286
- }),
287
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
288
- type: "button",
289
- title: "刷新",
290
- "aria-label": "刷新",
291
- onClick: () => setReloadToken((value) => value + 1),
292
- style: {
293
- border: "none",
294
- background: "transparent",
295
- color: "inherit",
296
- cursor: "pointer",
297
- display: "grid",
298
- placeItems: "center",
299
- width: 26,
300
- height: 26,
301
- borderRadius: 6
302
- },
303
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
304
- width: "14",
305
- height: "14",
306
- viewBox: "0 0 24 24",
307
- fill: "none",
308
- stroke: "currentColor",
309
- strokeWidth: "1.9",
310
- strokeLinecap: "round",
311
- strokeLinejoin: "round",
312
- "aria-hidden": "true",
313
- 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" })]
846
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CockpitViewer, {
847
+ artifactId: selected.viewerId,
848
+ reloadToken
314
849
  })
315
- }),
316
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
317
- type: "button",
318
- title: "全屏查看",
319
- "aria-label": "全屏查看",
320
- onClick: () => {
321
- const target = document.querySelector("[data-artifact-fullscreen-host]") ?? document.querySelector("iframe[data-artifact-viewer]")?.parentElement ?? null;
322
- if (target === null) return;
323
- if (document.fullscreenElement !== null) document.exitFullscreen();
324
- else target.requestFullscreen?.().catch(() => void 0);
325
- },
326
- style: {
327
- border: "none",
328
- background: "transparent",
329
- color: "inherit",
330
- cursor: "pointer",
331
- display: "grid",
332
- placeItems: "center",
333
- width: 26,
334
- height: 26,
335
- borderRadius: 6
336
- },
337
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
338
- width: "14",
339
- height: "14",
340
- viewBox: "0 0 24 24",
341
- fill: "none",
342
- stroke: "currentColor",
343
- strokeWidth: "1.9",
344
- strokeLinecap: "round",
345
- strokeLinejoin: "round",
346
- "aria-hidden": "true",
347
- children: [
348
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 9V4h5" }),
349
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M20 9V4h-5" }),
350
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 15v5h5" }),
351
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M20 15v5h-5" })
352
- ]
353
- })
354
- }),
355
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
356
- type: "button",
357
- title: "导出 HTML",
358
- "aria-label": "导出 HTML",
359
- disabled: exporting,
360
- onClick: () => void exportHtml(),
361
- style: {
362
- border: "none",
363
- background: "transparent",
364
- color: "inherit",
365
- cursor: exporting ? "progress" : "pointer",
366
- display: "grid",
367
- placeItems: "center",
368
- width: 26,
369
- height: 26,
370
- borderRadius: 6,
371
- opacity: exporting ? .5 : 1
372
- },
373
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
374
- width: "14",
375
- height: "14",
376
- viewBox: "0 0 24 24",
377
- fill: "none",
378
- stroke: "currentColor",
379
- strokeWidth: "1.9",
380
- strokeLinecap: "round",
381
- strokeLinejoin: "round",
382
- "aria-hidden": "true",
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",
383
871
  children: [
384
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 3v12" }),
385
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M7 10l5 5 5-5" }),
386
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 21h16" })
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
387
939
  ]
388
- })
940
+ })]
389
941
  })
390
- ]
391
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
392
- "data-artifact-fullscreen-host": true,
393
- style: {
394
- flex: 1,
395
- minHeight: 0
396
- },
397
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CockpitViewer, {
398
- artifactId: selected.id,
399
- reloadToken
400
- })
401
- })]
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
+ ]
402
949
  });
403
950
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
404
- style: {
405
- display: "flex",
406
- flexDirection: "column",
407
- height: "100%",
408
- minHeight: 0
409
- },
410
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
411
- style: {
412
- display: "flex",
413
- alignItems: "center",
414
- gap: 8,
415
- padding: "10px 12px",
416
- borderBottom: "1px solid var(--dsw-alias-border-l1, #e5e7eb)"
417
- },
418
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
419
- style: {
420
- flex: 1,
421
- minWidth: 0
422
- },
423
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
424
- style: {
425
- fontSize: 12,
426
- fontWeight: 600
427
- },
428
- children: "驾驶舱"
429
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
430
- style: muted,
431
- children: [
432
- "DataOps 驾驶舱资产 · ",
433
- apps.length,
434
- " 个",
435
- catalogs.length > 0 ? " · " + catalogs.join(" / ") : ""
436
- ]
437
- })]
438
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
439
- type: "button",
440
- onClick: () => void load(),
441
- style: {
442
- border: "none",
443
- background: "transparent",
444
- color: "inherit",
445
- cursor: "pointer",
446
- fontSize: 12
447
- },
448
- children: "刷新"
449
- })]
450
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
451
- style: {
452
- flex: 1,
453
- minHeight: 0,
454
- overflowY: "auto",
455
- padding: 10,
456
- display: "grid",
457
- gap: 8,
458
- alignContent: "start"
459
- },
460
- children: [
461
- loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
462
- style: {
463
- ...muted,
464
- textAlign: "center",
465
- padding: 18
466
- },
467
- children: "正在加载驾驶舱…"
468
- }) : null,
469
- error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
470
- role: "alert",
471
- style: {
472
- padding: "8px 10px",
473
- borderRadius: 8,
474
- background: "#fef2f2",
475
- color: "#b91c1c",
476
- fontSize: 11
477
- },
478
- children: error
479
- }) : null,
480
- !loading && error === null && apps.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
481
- style: {
482
- ...muted,
483
- textAlign: "center",
484
- padding: 18
485
- },
486
- children: "暂无驾驶舱"
487
- }) : null,
488
- apps.map((app) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
489
- type: "button",
490
- style: cardStyle,
491
- onClick: () => setSelected({
492
- id: app.id,
493
- name: app.name
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
+ })
494
979
  }),
495
- "data-cockpit-id": app.id,
496
- children: [
497
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
498
- style: {
499
- display: "flex",
500
- alignItems: "center",
501
- gap: 6,
502
- minWidth: 0
503
- },
504
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
505
- style: {
506
- flex: 1,
507
- minWidth: 0,
508
- fontSize: 12,
509
- fontWeight: 600,
510
- overflow: "hidden",
511
- textOverflow: "ellipsis",
512
- whiteSpace: "nowrap"
513
- },
514
- children: app.name
515
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
516
- style: {
517
- ...muted,
518
- flex: "none"
519
- },
520
- children: app.status === "PUBLISHED" ? "已发布" : "草稿"
521
- })]
522
- }),
523
- app.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
524
- style: {
525
- ...muted,
526
- overflow: "hidden",
527
- textOverflow: "ellipsis",
528
- whiteSpace: "nowrap"
529
- },
530
- children: app.description
531
- }) : null,
532
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
533
- style: muted,
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",
534
987
  children: [
535
- app.catalog ?? "未分类",
536
- " · v",
537
- app.publishedVersion || app.currentVersion
988
+ apps.length,
989
+ " 个应用 · ",
990
+ templates.length,
991
+ " 个模板 · ",
992
+ components.length,
993
+ " 个组件",
994
+ catalogs.length > 0 ? " · " + catalogs.join(" / ") : ""
538
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" })]
539
1015
  })
540
- ]
541
- }, app.id))
542
- ]
543
- })]
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
+ ]
544
1286
  });
545
1287
  }
546
1288
  //#endregion
@@ -675,6 +1417,8 @@ window.__ModuleLoader__.load({
675
1417
  */
676
1418
  /** Client services this plugin reads; `slots` owns the always-mounted bridge seat. */
677
1419
  const inject = ["slots"];
1420
+ /** Reference source id shared by the chip codec and the inserted reference. */
1421
+ const REFERENCE_SOURCE = "dataops-artifact-app";
678
1422
  /** Tab id shared by registration, the bridge and the tools. */
679
1423
  const ARTIFACT_TAB_TYPE = "dataops-artifact-apps";
680
1424
  /** File viewer id. */
@@ -747,16 +1491,79 @@ window.__ModuleLoader__.load({
747
1491
  let disposeBridge;
748
1492
  let betterSidebarService;
749
1493
  const inject = ctx.inject;
750
- inject?.(["betterSidebar"], (injected) => {
1494
+ inject?.([
1495
+ "betterSidebar",
1496
+ "conversation",
1497
+ "sessions",
1498
+ "inputTriggers"
1499
+ ], (injected) => {
751
1500
  const service = injected.betterSidebar;
752
1501
  if (service?.registerTab === void 0) return;
753
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
+ };
754
1561
  disposeTab = service.registerTab({
755
1562
  id: ARTIFACT_TAB_TYPE,
756
1563
  title: "驾驶舱",
757
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
758
- width: "14",
759
- height: "14",
1564
+ icon: ({ size = 14 }) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1565
+ width: size >= 22 ? 22 : size,
1566
+ height: size >= 22 ? 22 : size,
760
1567
  viewBox: "0 0 24 24",
761
1568
  fill: "none",
762
1569
  stroke: "currentColor",
@@ -777,7 +1584,10 @@ window.__ModuleLoader__.load({
777
1584
  description: "DataOps 驾驶舱资产、预览与发布",
778
1585
  order: 62,
779
1586
  single: true,
780
- component: ((props) => CockpitTab(props))
1587
+ component: ((props) => CockpitTab({
1588
+ ...props,
1589
+ insertArtifactReference
1590
+ }))
781
1591
  });
782
1592
  disposeViewer = service.registerFileViewer?.({
783
1593
  id: ARTIFACT_VIEWER_ID,