dsh-cloudq 0.1.9 → 0.2.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
@@ -408,6 +408,46 @@ linear-gradient(-30deg, transparent 49.5%, #2c2c31 49.5%, #2c2c31 50.5%, transpa
408
408
  overflow: hidden;
409
409
  text-overflow: ellipsis;
410
410
  }
411
+ /* Outdated-plugin badge appended after the sidebar button label. */
412
+ .dsh-cloudq-version-badge {
413
+ position: relative;
414
+ flex: none;
415
+ display: inline-flex;
416
+ align-items: center;
417
+ justify-content: center;
418
+ width: 16px;
419
+ height: 16px;
420
+ margin-left: 6px;
421
+ border-radius: 50%;
422
+ background: var(--dsw-alias-state-warning, #d18400);
423
+ color: #fff;
424
+ font-size: 11px;
425
+ font-weight: 700;
426
+ line-height: 16px;
427
+ cursor: pointer;
428
+ }
429
+ .dsh-cloudq-version-badge.is-busy {
430
+ opacity: .7;
431
+ cursor: default;
432
+ }
433
+ .dsh-cloudq-version-badge__tip {
434
+ display: none;
435
+ position: fixed;
436
+ z-index: 1000;
437
+ padding: 6px 10px;
438
+ border-radius: 6px;
439
+ background: rgba(20, 24, 31, .92);
440
+ color: #fff;
441
+ font-size: 12px;
442
+ font-weight: 400;
443
+ line-height: 18px;
444
+ white-space: nowrap;
445
+ box-shadow: 0 4px 12px rgba(0, 0, 0, .18);
446
+ }
447
+ .dsh-cloudq-version-badge:hover .dsh-cloudq-version-badge__tip,
448
+ .dsh-cloudq-version-badge:focus .dsh-cloudq-version-badge__tip {
449
+ display: block;
450
+ }
411
451
  /* Persisted CloudQ marker shown on rows whose session id is in the registry. */
412
452
  .dsh-cloudq-session-badge {
413
453
  flex: none;
@@ -773,6 +813,75 @@ linear-gradient(-30deg, transparent 49.5%, #2c2c31 49.5%, #2c2c31 50.5%, transpa
773
813
  if (button) button.remove();
774
814
  };
775
815
  }
816
+ const API_VERSION = "/api/dsh-cloudq/version";
817
+ const API_UPDATE = "/api/dsh-cloudq/update";
818
+ const API_RESTART = "/api/dsh-cloudq/restart";
819
+ let cloudqOutdatedInfo = null;
820
+ let cloudqSelfUpdateRunning = false;
821
+ function positionVersionTip(badge, tip) {
822
+ const rect = badge.getBoundingClientRect();
823
+ tip.style.left = `${Math.max(8, rect.left)}px`;
824
+ tip.style.top = `${rect.bottom + 6}px`;
825
+ tip.style.bottom = "auto";
826
+ }
827
+ /** Append the "!" badge after the sidebar button label (retries while the
828
+ * button is not mounted yet). The button node is reused across sidebar
829
+ * re-mounts, so a child badge survives them. */
830
+ function attachVersionBadge(attempt = 0) {
831
+ if (!cloudqOutdatedInfo) return;
832
+ const button = document.getElementById("dsh-cloudq-sidebar-entry");
833
+ if (!button) {
834
+ if (attempt < 20) window.setTimeout(() => attachVersionBadge(attempt + 1), 500);
835
+ return;
836
+ }
837
+ if (button.querySelector(".dsh-cloudq-version-badge")) return;
838
+ const badge = document.createElement("span");
839
+ badge.className = "dsh-cloudq-version-badge";
840
+ badge.dataset.testid = "cloudq-version-badge";
841
+ badge.textContent = "!";
842
+ badge.setAttribute("role", "button");
843
+ badge.setAttribute("aria-label", `CloudQ 插件有新版本 ${cloudqOutdatedInfo.latest},点击更新`);
844
+ const tip = document.createElement("span");
845
+ tip.className = "dsh-cloudq-version-badge__tip";
846
+ tip.textContent = `当前版本 ${cloudqOutdatedInfo.current} 不是最新版本 ${cloudqOutdatedInfo.latest},点击自动更新`;
847
+ badge.appendChild(tip);
848
+ badge.addEventListener("mouseenter", () => positionVersionTip(badge, tip));
849
+ badge.addEventListener("click", (event) => {
850
+ event.preventDefault();
851
+ event.stopPropagation();
852
+ runCloudqSelfUpdate(badge, tip);
853
+ });
854
+ button.appendChild(badge);
855
+ }
856
+ /** Badge click flow: update → restart host → wait for it → reload page. */
857
+ async function runCloudqSelfUpdate(badge, tip) {
858
+ if (cloudqSelfUpdateRunning) return;
859
+ cloudqSelfUpdateRunning = true;
860
+ badge.classList.add("is-busy");
861
+ tip.textContent = "正在更新到最新版本…";
862
+ try {
863
+ await cloudqRequest(API_UPDATE, { method: "POST" }, 15e4);
864
+ } catch (error) {
865
+ tip.textContent = `更新失败:${error.message}`;
866
+ badge.classList.remove("is-busy");
867
+ cloudqSelfUpdateRunning = false;
868
+ return;
869
+ }
870
+ tip.textContent = "更新完成,正在重启 DSH 服务…";
871
+ try {
872
+ await cloudqRequest(API_RESTART, { method: "POST" }, 5e3);
873
+ } catch {}
874
+ const sleep = (ms) => new Promise((resolvePromise) => window.setTimeout(resolvePromise, ms));
875
+ const deadline = Date.now() + 6e4;
876
+ while (Date.now() < deadline) try {
877
+ await cloudqRequest(API_VERSION, void 0, 3e3);
878
+ window.location.reload();
879
+ return;
880
+ } catch {
881
+ await sleep(1e3);
882
+ }
883
+ tip.textContent = "服务重启超时,请手动重启 DSH 后刷新页面。";
884
+ }
776
885
  /** Build the exact visible row order from Host workspace membership. */
777
886
  function orderedVisibleSessions(snapshot, workspaces, sortByUpdated) {
778
887
  const archived = new Set(workspaces?.archivedSessionIds ?? []);
@@ -1014,7 +1123,9 @@ body.dsh-cloudq-mode-active [class*=composerHero] [class*=root][class*=hero] {
1014
1123
  }
1015
1124
  .dsh-cloudq-convo-chip {
1016
1125
  position: fixed;
1017
- top: 20px;
1126
+ /* Below the conversation header divider (header bottom ≈76px) so the chip
1127
+ never overlaps the Session log affordance in the title row. */
1128
+ top: 88px;
1018
1129
  right: 16px;
1019
1130
  display: inline-flex;
1020
1131
  align-items: center;
@@ -1605,12 +1716,24 @@ body.dsh-cloudq-mode-active [class*=composerHero] [class*=root][class*=hero] {
1605
1716
  }
1606
1717
  .dsh-cloudq-artifact__download {
1607
1718
  justify-self: end;
1719
+ padding: 0;
1720
+ border: 0;
1721
+ background: none;
1608
1722
  color: var(--dsw-alias-state-business-primary, #006eff);
1723
+ font: inherit;
1609
1724
  text-decoration: none;
1725
+ cursor: pointer;
1610
1726
  }
1611
1727
  .dsh-cloudq-artifact__download:hover {
1612
1728
  text-decoration: underline;
1613
1729
  }
1730
+ .dsh-cloudq-artifact__download:disabled {
1731
+ cursor: default;
1732
+ opacity: .6;
1733
+ }
1734
+ span.dsh-cloudq-artifact__download {
1735
+ cursor: default;
1736
+ }
1614
1737
  /* architecture library */
1615
1738
  .dsh-cloudq-arch__toolbar {
1616
1739
  position: relative;
@@ -2036,12 +2159,10 @@ display: none;
2036
2159
  let panelBody = null;
2037
2160
  let panelView = "usage";
2038
2161
  let panelExpanded = false;
2039
- let inspirationCache = null;
2040
2162
  let inspirationCategory = 0;
2041
- let artifactCache = null;
2042
- let artifactTotal = 0;
2043
- let architectureDirectoriesCache = null;
2044
- let architectureFirstFolderId = null;
2163
+ const artifactCollapsedSessions = /* @__PURE__ */ new Set();
2164
+ let artifactRefreshTimer = 0;
2165
+ let artifactRetryTimer = 0;
2045
2166
  let architectureRequestSeq = 0;
2046
2167
  let architecturePickerCleanup = null;
2047
2168
  function renderUsageView() {
@@ -2345,17 +2466,12 @@ display: none;
2345
2466
  list.appendChild(card);
2346
2467
  }
2347
2468
  };
2348
- if (inspirationCache) {
2349
- paint(inspirationCache);
2350
- return;
2351
- }
2352
2469
  const hint = document.createElement("div");
2353
2470
  hint.className = "dsh-cloudq-panel__hint";
2354
2471
  hint.textContent = "正在加载灵感…";
2355
2472
  list.appendChild(hint);
2356
2473
  requireCloudqCredential().then(() => cloudqRequest(API_INSPIRATIONS)).then((data) => {
2357
- inspirationCache = Array.isArray(data.inspirations) ? data.inspirations : [];
2358
- paint(inspirationCache);
2474
+ paint(Array.isArray(data.inspirations) ? data.inspirations : []);
2359
2475
  }).catch((error) => {
2360
2476
  if (!panelExpanded) return;
2361
2477
  list.textContent = "";
@@ -2365,140 +2481,181 @@ display: none;
2365
2481
  list.appendChild(err);
2366
2482
  });
2367
2483
  }
2368
- function renderArtifactView({ force = false } = {}) {
2484
+ /**
2485
+ * Refresh the artifact list in the background after a CloudQ conversation
2486
+ * turn completes. Never shows the loading hint: the visible list stays
2487
+ * untouched and is repainted synchronously once fresh data arrives. The
2488
+ * delayed retry covers artifacts archived with a lag.
2489
+ */
2490
+ function scheduleArtifactSilentRefresh() {
2491
+ window.clearTimeout(artifactRefreshTimer);
2492
+ window.clearTimeout(artifactRetryTimer);
2493
+ const run = () => {
2494
+ cloudqRequest(API_ARTIFACTS).then((data) => {
2495
+ if (panelView !== "artifact" || !panelExpanded) return;
2496
+ paintArtifactList(Array.isArray(data?.sessions) ? data.sessions : [], Number(data?.total) || 0);
2497
+ }).catch(() => {});
2498
+ };
2499
+ artifactRefreshTimer = window.setTimeout(run, 3e3);
2500
+ artifactRetryTimer = window.setTimeout(run, 15e3);
2501
+ }
2502
+ function paintArtifactList(sessions, total) {
2503
+ if (panelView !== "artifact" || !panelExpanded) return;
2369
2504
  panelBody.textContent = "";
2370
- const paint = (sessions, total) => {
2371
- if (panelView !== "artifact" || !panelExpanded) return;
2372
- panelBody.textContent = "";
2373
- const safeSessions = Array.isArray(sessions) ? sessions : [];
2374
- const artifactCount = safeSessions.reduce((sum, session) => {
2375
- return sum + (Array.isArray(session?.Artifacts) ? session.Artifacts : []).length;
2376
- }, 0);
2377
- const toolbar = document.createElement("div");
2378
- toolbar.className = "dsh-cloudq-artifact__toolbar";
2379
- const summary = document.createElement("span");
2380
- summary.textContent = `共 ${Number(total) || safeSessions.length} 组会话,${artifactCount} 个制品`;
2381
- const refresh = document.createElement("button");
2382
- refresh.type = "button";
2383
- refresh.className = "dsh-cloudq-artifact__refresh";
2384
- refresh.dataset.testid = "cloudq-artifact-refresh-btn";
2385
- refresh.textContent = "刷新";
2386
- refresh.addEventListener("click", () => {
2387
- artifactCache = null;
2388
- artifactTotal = 0;
2389
- renderArtifactView({ force: true });
2390
- });
2391
- toolbar.appendChild(summary);
2392
- toolbar.appendChild(refresh);
2393
- panelBody.appendChild(toolbar);
2394
- if (safeSessions.length === 0) {
2395
- const empty = document.createElement("div");
2396
- empty.className = "dsh-cloudq-usage__table__empty";
2397
- empty.textContent = "暂无制品";
2398
- panelBody.appendChild(empty);
2399
- return;
2400
- }
2401
- const list = document.createElement("div");
2402
- list.className = "dsh-cloudq-artifact__list";
2403
- const columns = document.createElement("div");
2404
- columns.className = "dsh-cloudq-artifact__columns";
2405
- for (const label of [
2406
- "报告名称",
2407
- "用户名称",
2408
- "生成时间",
2409
- "操作"
2410
- ]) {
2411
- const cell = document.createElement("span");
2412
- cell.textContent = label;
2413
- columns.appendChild(cell);
2414
- }
2415
- list.appendChild(columns);
2416
- for (const session of safeSessions) {
2417
- const artifacts = Array.isArray(session?.Artifacts) ? session.Artifacts : [];
2418
- const group = document.createElement("section");
2419
- group.className = "dsh-cloudq-artifact__session";
2420
- const sessionHead = document.createElement("button");
2421
- sessionHead.type = "button";
2422
- sessionHead.className = "dsh-cloudq-artifact__session-head";
2423
- sessionHead.dataset.testid = `cloudq-artifact-session-${session?.SessionID ?? "unknown"}`;
2424
- sessionHead.setAttribute("aria-expanded", "true");
2425
- const chevron = document.createElement("span");
2426
- chevron.className = "dsh-cloudq-artifact__chevron";
2427
- chevron.textContent = "▼";
2428
- const title = document.createElement("span");
2429
- title.className = "dsh-cloudq-artifact__session-title";
2430
- title.textContent = session?.SessionTitle || "未命名会话";
2431
- title.title = title.textContent;
2432
- const latestTime = artifacts.reduce((latest, artifact) => {
2433
- const time = String(artifact?.ArchiveTime ?? "");
2434
- return time > latest ? time : latest;
2435
- }, "");
2436
- const meta = document.createElement("span");
2437
- meta.className = "dsh-cloudq-artifact__session-meta";
2438
- meta.textContent = `${artifacts.length} 个制品${latestTime ? ` · 最近 ${latestTime.slice(0, 10)}` : ""}`;
2439
- sessionHead.appendChild(chevron);
2440
- sessionHead.appendChild(title);
2441
- sessionHead.appendChild(meta);
2442
- const files = document.createElement("div");
2443
- files.className = "dsh-cloudq-artifact__files";
2444
- for (const artifact of artifacts) {
2445
- const row = document.createElement("div");
2446
- row.className = "dsh-cloudq-artifact__file";
2447
- const main = document.createElement("div");
2448
- main.className = "dsh-cloudq-artifact__file-main";
2449
- const type = document.createElement("span");
2450
- type.className = "dsh-cloudq-artifact__file-type";
2451
- type.textContent = artifact?.FileType || "file";
2452
- const name = document.createElement("span");
2453
- name.className = "dsh-cloudq-artifact__file-name";
2454
- name.textContent = artifact?.FileName || "未命名文件";
2455
- name.title = `${name.textContent} · ${formatFileSize(artifact?.SizeBytes)}`;
2456
- main.appendChild(type);
2457
- main.appendChild(name);
2458
- const owner = document.createElement("span");
2459
- owner.className = "dsh-cloudq-artifact__owner";
2460
- owner.textContent = artifact?.UserName || "—";
2461
- const time = document.createElement("span");
2462
- time.className = "dsh-cloudq-artifact__time";
2463
- time.textContent = formatDateTime(artifact?.ArchiveTime);
2464
- const downloadUrl = safeDownloadUrl(artifact?.DownloadURL);
2465
- const action = document.createElement(downloadUrl ? "a" : "span");
2466
- action.className = "dsh-cloudq-artifact__download";
2467
- action.textContent = downloadUrl ? "下载" : "不可用";
2468
- if (downloadUrl) {
2469
- action.href = downloadUrl;
2470
- action.target = "_blank";
2471
- action.rel = "noopener noreferrer";
2472
- action.dataset.testid = `cloudq-artifact-download-${artifact?.ArtifactID ?? "unknown"}`;
2473
- }
2474
- row.appendChild(main);
2475
- row.appendChild(owner);
2476
- row.appendChild(time);
2477
- row.appendChild(action);
2478
- files.appendChild(row);
2505
+ const safeSessions = Array.isArray(sessions) ? sessions : [];
2506
+ const artifactCount = safeSessions.reduce((sum, session) => {
2507
+ return sum + (Array.isArray(session?.Artifacts) ? session.Artifacts : []).length;
2508
+ }, 0);
2509
+ const toolbar = document.createElement("div");
2510
+ toolbar.className = "dsh-cloudq-artifact__toolbar";
2511
+ const summary = document.createElement("span");
2512
+ summary.textContent = `共 ${Number(total) || safeSessions.length} 组会话,${artifactCount} 个制品`;
2513
+ const refresh = document.createElement("button");
2514
+ refresh.type = "button";
2515
+ refresh.className = "dsh-cloudq-artifact__refresh";
2516
+ refresh.dataset.testid = "cloudq-artifact-refresh-btn";
2517
+ refresh.textContent = "刷新";
2518
+ refresh.addEventListener("click", () => renderArtifactView());
2519
+ toolbar.appendChild(summary);
2520
+ toolbar.appendChild(refresh);
2521
+ panelBody.appendChild(toolbar);
2522
+ if (safeSessions.length === 0) {
2523
+ const empty = document.createElement("div");
2524
+ empty.className = "dsh-cloudq-usage__table__empty";
2525
+ empty.textContent = "暂无制品";
2526
+ panelBody.appendChild(empty);
2527
+ return;
2528
+ }
2529
+ const list = document.createElement("div");
2530
+ list.className = "dsh-cloudq-artifact__list";
2531
+ const columns = document.createElement("div");
2532
+ columns.className = "dsh-cloudq-artifact__columns";
2533
+ for (const label of [
2534
+ "报告名称",
2535
+ "用户名称",
2536
+ "生成时间",
2537
+ "操作"
2538
+ ]) {
2539
+ const cell = document.createElement("span");
2540
+ cell.textContent = label;
2541
+ columns.appendChild(cell);
2542
+ }
2543
+ list.appendChild(columns);
2544
+ for (const session of safeSessions) {
2545
+ const artifacts = Array.isArray(session?.Artifacts) ? session.Artifacts : [];
2546
+ const sessionKey = String(session?.SessionID ?? "");
2547
+ const group = document.createElement("section");
2548
+ group.className = "dsh-cloudq-artifact__session";
2549
+ const startCollapsed = sessionKey !== "" && artifactCollapsedSessions.has(sessionKey);
2550
+ if (startCollapsed) group.classList.add("is-collapsed");
2551
+ const sessionHead = document.createElement("button");
2552
+ sessionHead.type = "button";
2553
+ sessionHead.className = "dsh-cloudq-artifact__session-head";
2554
+ sessionHead.dataset.testid = `cloudq-artifact-session-${session?.SessionID ?? "unknown"}`;
2555
+ sessionHead.setAttribute("aria-expanded", String(!startCollapsed));
2556
+ const chevron = document.createElement("span");
2557
+ chevron.className = "dsh-cloudq-artifact__chevron";
2558
+ chevron.textContent = "▼";
2559
+ const title = document.createElement("span");
2560
+ title.className = "dsh-cloudq-artifact__session-title";
2561
+ title.textContent = session?.SessionTitle || "未命名会话";
2562
+ title.title = title.textContent;
2563
+ const latestTime = artifacts.reduce((latest, artifact) => {
2564
+ const time = String(artifact?.ArchiveTime ?? "");
2565
+ return time > latest ? time : latest;
2566
+ }, "");
2567
+ const meta = document.createElement("span");
2568
+ meta.className = "dsh-cloudq-artifact__session-meta";
2569
+ meta.textContent = `${artifacts.length} 个制品${latestTime ? ` · 最近 ${latestTime.slice(0, 10)}` : ""}`;
2570
+ sessionHead.appendChild(chevron);
2571
+ sessionHead.appendChild(title);
2572
+ sessionHead.appendChild(meta);
2573
+ const files = document.createElement("div");
2574
+ files.className = "dsh-cloudq-artifact__files";
2575
+ for (const artifact of artifacts) {
2576
+ const row = document.createElement("div");
2577
+ row.className = "dsh-cloudq-artifact__file";
2578
+ const main = document.createElement("div");
2579
+ main.className = "dsh-cloudq-artifact__file-main";
2580
+ const type = document.createElement("span");
2581
+ type.className = "dsh-cloudq-artifact__file-type";
2582
+ type.textContent = artifact?.FileType || "file";
2583
+ const name = document.createElement("span");
2584
+ name.className = "dsh-cloudq-artifact__file-name";
2585
+ name.textContent = artifact?.FileName || "未命名文件";
2586
+ name.title = `${name.textContent} · ${formatFileSize(artifact?.SizeBytes)}`;
2587
+ main.appendChild(type);
2588
+ main.appendChild(name);
2589
+ const owner = document.createElement("span");
2590
+ owner.className = "dsh-cloudq-artifact__owner";
2591
+ owner.textContent = artifact?.UserName || "—";
2592
+ const time = document.createElement("span");
2593
+ time.className = "dsh-cloudq-artifact__time";
2594
+ time.textContent = formatDateTime(artifact?.ArchiveTime);
2595
+ const downloadUrl = safeDownloadUrl(artifact?.DownloadURL);
2596
+ const fileName = artifact?.FileName || "未命名文件";
2597
+ const action = document.createElement(downloadUrl ? "button" : "span");
2598
+ action.className = "dsh-cloudq-artifact__download";
2599
+ action.textContent = downloadUrl ? "下载" : "不可用";
2600
+ if (downloadUrl) {
2601
+ action.type = "button";
2602
+ action.dataset.testid = `cloudq-artifact-download-${artifact?.ArtifactID ?? "unknown"}`;
2603
+ action.addEventListener("click", () => {
2604
+ if (action.disabled) return;
2605
+ action.disabled = true;
2606
+ action.textContent = "下载中…";
2607
+ const restore = () => {
2608
+ action.disabled = false;
2609
+ action.textContent = "下载";
2610
+ };
2611
+ fetch(downloadUrl).then((res) => {
2612
+ if (!res.ok) throw new Error(`download failed: ${res.status}`);
2613
+ return res.blob();
2614
+ }).then((blob) => {
2615
+ const objectUrl = URL.createObjectURL(blob);
2616
+ const link = document.createElement("a");
2617
+ link.href = objectUrl;
2618
+ link.download = fileName;
2619
+ link.rel = "noopener noreferrer";
2620
+ link.click();
2621
+ window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1e4);
2622
+ }).catch(() => {
2623
+ const link = document.createElement("a");
2624
+ link.href = downloadUrl;
2625
+ link.download = fileName;
2626
+ link.rel = "noopener noreferrer";
2627
+ link.click();
2628
+ }).finally(restore);
2629
+ });
2479
2630
  }
2480
- sessionHead.addEventListener("click", () => {
2481
- const collapsed = group.classList.toggle("is-collapsed");
2482
- sessionHead.setAttribute("aria-expanded", String(!collapsed));
2483
- });
2484
- group.appendChild(sessionHead);
2485
- group.appendChild(files);
2486
- list.appendChild(group);
2631
+ row.appendChild(main);
2632
+ row.appendChild(owner);
2633
+ row.appendChild(time);
2634
+ row.appendChild(action);
2635
+ files.appendChild(row);
2487
2636
  }
2488
- panelBody.appendChild(list);
2489
- };
2490
- if (artifactCache !== null && !force) {
2491
- paint(artifactCache, artifactTotal);
2492
- return;
2637
+ sessionHead.addEventListener("click", () => {
2638
+ const collapsed = group.classList.toggle("is-collapsed");
2639
+ sessionHead.setAttribute("aria-expanded", String(!collapsed));
2640
+ if (sessionKey) {
2641
+ if (collapsed) artifactCollapsedSessions.add(sessionKey);
2642
+ else artifactCollapsedSessions.delete(sessionKey);
2643
+ }
2644
+ });
2645
+ group.appendChild(sessionHead);
2646
+ group.appendChild(files);
2647
+ list.appendChild(group);
2493
2648
  }
2649
+ panelBody.appendChild(list);
2650
+ }
2651
+ function renderArtifactView() {
2652
+ panelBody.textContent = "";
2494
2653
  const hint = document.createElement("div");
2495
2654
  hint.className = "dsh-cloudq-panel__hint";
2496
2655
  hint.textContent = "正在加载制品…";
2497
2656
  panelBody.appendChild(hint);
2498
2657
  requireCloudqCredential().then(() => cloudqRequest(API_ARTIFACTS)).then((data) => {
2499
- artifactCache = Array.isArray(data?.sessions) ? data.sessions : [];
2500
- artifactTotal = Number(data?.total) || artifactCache.length;
2501
- paint(artifactCache, artifactTotal);
2658
+ paintArtifactList(Array.isArray(data?.sessions) ? data.sessions : [], Number(data?.total) || 0);
2502
2659
  }).catch((error) => {
2503
2660
  if (panelView !== "artifact" || !panelExpanded) return;
2504
2661
  panelBody.textContent = "";
@@ -2733,14 +2890,16 @@ display: none;
2733
2890
  }
2734
2891
  for (const architecture of architectures) {
2735
2892
  const imageUrl = safeDownloadUrl(architecture?.SvgURL);
2736
- const card = document.createElement(imageUrl ? "a" : "article");
2893
+ const archId = typeof architecture?.ArchId === "string" ? architecture.ArchId.trim() : "";
2894
+ const consoleUrl = archId ? `https://console.cloud.tencent.com/advisor?archId=${encodeURIComponent(archId)}` : "";
2895
+ const card = document.createElement(consoleUrl ? "a" : "article");
2737
2896
  card.className = "dsh-cloudq-arch__card";
2738
2897
  card.dataset.testid = `cloudq-architecture-card-${architecture?.ArchId ?? "unknown"}`;
2739
- if (imageUrl) {
2740
- card.href = imageUrl;
2898
+ if (consoleUrl) {
2899
+ card.href = consoleUrl;
2741
2900
  card.target = "_blank";
2742
2901
  card.rel = "noopener noreferrer";
2743
- card.setAttribute("aria-label", `查看架构图:${architecture?.ArchName || "未命名系统"}`);
2902
+ card.setAttribute("aria-label", `打开架构图:${architecture?.ArchName || "未命名系统"}`);
2744
2903
  }
2745
2904
  const preview = document.createElement("div");
2746
2905
  preview.className = "dsh-cloudq-arch__preview";
@@ -2759,7 +2918,7 @@ display: none;
2759
2918
  image.addEventListener("load", () => placeholder.remove());
2760
2919
  image.addEventListener("error", () => {
2761
2920
  image.remove();
2762
- placeholder.textContent = "预览加载失败,点击可打开原图";
2921
+ placeholder.textContent = consoleUrl ? "预览加载失败,点击打开架构图" : "预览加载失败";
2763
2922
  });
2764
2923
  preview.appendChild(image);
2765
2924
  }
@@ -2791,14 +2950,8 @@ display: none;
2791
2950
  };
2792
2951
  loadArchitectures(selectedFolderId);
2793
2952
  };
2794
- if (architectureDirectoriesCache !== null) {
2795
- paintDirectories(architectureDirectoriesCache, architectureFirstFolderId);
2796
- return;
2797
- }
2798
2953
  requireCloudqCredential().then(() => cloudqRequest(API_ARCH_DIRECTORIES)).then((data) => {
2799
- architectureDirectoriesCache = Array.isArray(data?.folders) ? data.folders : [];
2800
- architectureFirstFolderId = Number(data?.firstFolderId) || null;
2801
- paintDirectories(architectureDirectoriesCache, architectureFirstFolderId);
2954
+ paintDirectories(Array.isArray(data?.folders) ? data.folders : [], Number(data?.firstFolderId) || null);
2802
2955
  }).catch((error) => {
2803
2956
  if (panelView !== "architecture" || !panelExpanded || viewRequest !== architectureRequestSeq) return;
2804
2957
  panelBody.textContent = "";
@@ -3073,12 +3226,7 @@ display: none;
3073
3226
  panelEl = null;
3074
3227
  panelBody = null;
3075
3228
  }
3076
- inspirationCache = null;
3077
3229
  inspirationCategory = 0;
3078
- artifactCache = null;
3079
- artifactTotal = 0;
3080
- architectureDirectoriesCache = null;
3081
- architectureFirstFolderId = null;
3082
3230
  architectureRequestSeq += 1;
3083
3231
  panelView = "usage";
3084
3232
  };
@@ -3237,7 +3385,7 @@ display: none;
3237
3385
  });
3238
3386
  } catch (error) {
3239
3387
  setValidated(false);
3240
- const invalid = error instanceof CloudQApiError && error.code !== "network-error" && error.code !== "invalid-response";
3388
+ const invalid = error instanceof CloudQApiError && error.code !== "network-error" && error.code !== "invalid-response" && error.code !== "script-launch-failed";
3241
3389
  setFeedback({
3242
3390
  kind: "error",
3243
3391
  text: invalid ? "AKSK 无效,请检查后重新配置。" : error.message
@@ -3644,6 +3792,33 @@ display: none;
3644
3792
  window.removeEventListener("storage", onStorage);
3645
3793
  };
3646
3794
  }, "dsh-cloudq: reconcile durable session identities");
3795
+ ctx.effect(() => {
3796
+ const list = ctx?.sessions?.list;
3797
+ if (!list?.subscribe || !list?.getSnapshot) return void 0;
3798
+ const prevRunning = /* @__PURE__ */ new Map();
3799
+ return list.subscribe(() => {
3800
+ const byId = list.getSnapshot()?.byId ?? {};
3801
+ for (const [id, summary] of Object.entries(byId)) {
3802
+ const running = summary?.running === true;
3803
+ if (prevRunning.get(id) === true && !running && cloudqSessions.has(id)) scheduleArtifactSilentRefresh();
3804
+ prevRunning.set(id, running);
3805
+ }
3806
+ });
3807
+ }, "dsh-cloudq: refresh artifacts on turn completion");
3808
+ ctx.effect(() => {
3809
+ let disposed = false;
3810
+ cloudqRequest(API_VERSION).then((data) => {
3811
+ if (disposed || data?.outdated !== true || typeof data?.latest !== "string") return;
3812
+ cloudqOutdatedInfo = {
3813
+ current: String(data.current ?? ""),
3814
+ latest: data.latest
3815
+ };
3816
+ attachVersionBadge();
3817
+ }).catch(() => {});
3818
+ return () => {
3819
+ disposed = true;
3820
+ };
3821
+ }, "dsh-cloudq: version check");
3647
3822
  ctx.effect(() => {
3648
3823
  const markIfCloudqClaim = () => {
3649
3824
  const textarea = document.querySelector("textarea[class*=input]");
package/lib/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { createRequire } from "node:module";
2
+ import { spawn, spawnSync } from "node:child_process";
2
3
  import { existsSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { get } from "node:https";
3
5
  import { dirname, join, resolve } from "node:path";
4
6
  import { fileURLToPath } from "node:url";
5
7
  import Schema from "@deepseek-ai/schemastery";
6
8
  import { Buffer as Buffer$1 } from "node:buffer";
7
9
  import yaml from "js-yaml";
8
- import { spawn } from "node:child_process";
9
10
  //#region src/http.js
10
11
  /** Maximum accepted JSON request body size. */
11
12
  const MAX_JSON_BODY_BYTES = 65536;
@@ -147,13 +148,13 @@ function readJsonBody(request, { maxBytes = MAX_JSON_BODY_BYTES } = {}) {
147
148
  //#region src/plugin-manager.js
148
149
  /** Host-side management of optional profile bundle entries. */
149
150
  const PROTECTED_BUNDLES = /* @__PURE__ */ new Set(["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"]);
150
- function profileDirectory(baseUrl) {
151
+ function profileDirectory$1(baseUrl) {
151
152
  const url = new URL(".", baseUrl);
152
153
  if (url.protocol !== "file:") throw new Error("The active DSH profile URL must use the file protocol.");
153
154
  return fileURLToPath(url);
154
155
  }
155
156
  function patchPath(baseUrl) {
156
- return resolve(profileDirectory(baseUrl), "cordis.patch.yml");
157
+ return resolve(profileDirectory$1(baseUrl), "cordis.patch.yml");
157
158
  }
158
159
  function parsePatchList(content) {
159
160
  if (!content.trim()) return [];
@@ -226,7 +227,7 @@ function writePatchAtomically(path, content, originalSnapshot) {
226
227
  * @returns {Array<{id: string, name: string, bundle: string, disabled: boolean, self: boolean}>}
227
228
  */
228
229
  function listPlugins(baseUrl) {
229
- const profileDir = profileDirectory(baseUrl);
230
+ const profileDir = profileDirectory$1(baseUrl);
230
231
  const userPatchPath = patchPath(baseUrl);
231
232
  const overrides = (existsSync(userPatchPath) ? parsePatchList(readFileSync(userPatchPath, "utf8")) : []).filter((entry) => entry && typeof entry === "object" && typeof entry.id === "string");
232
233
  const plugins = [];
@@ -289,6 +290,24 @@ function setPluginDisabled(baseUrl, id, disabled) {
289
290
  //#endregion
290
291
  //#region src/script-runner.js
291
292
  const MAX_SCRIPT_OUTPUT_BYTES = 1048576;
293
+ let resolvedPythonCommand = null;
294
+ function pythonCommand() {
295
+ if (resolvedPythonCommand) return resolvedPythonCommand;
296
+ for (const candidate of [
297
+ "python3",
298
+ "python",
299
+ "py"
300
+ ]) try {
301
+ if (spawnSync(candidate, ["--version"], {
302
+ stdio: "ignore",
303
+ timeout: 5e3
304
+ }).status === 0) {
305
+ resolvedPythonCommand = candidate;
306
+ return candidate;
307
+ }
308
+ } catch {}
309
+ return "python3";
310
+ }
292
311
  function safeCode(value) {
293
312
  return typeof value === "string" && /^[a-zA-Z0-9._-]{1,80}$/.test(value) ? value : "script-failed";
294
313
  }
@@ -307,7 +326,8 @@ function redact(value, sensitiveValues) {
307
326
  */
308
327
  function runScript$1(scriptsDirectory, scriptName, args, { timeoutMs = 3e4, jsonOnly = true, stdin, sensitiveValues = [], spawnProcess = spawn } = {}) {
309
328
  return new Promise((resolveRun, rejectRun) => {
310
- const child = spawnProcess("python3", [resolve(scriptsDirectory, scriptName), ...args], {
329
+ const script = resolve(scriptsDirectory, scriptName);
330
+ const child = spawnProcess(pythonCommand(), [script, ...args], {
311
331
  stdio: [
312
332
  stdin === void 0 ? "ignore" : "pipe",
313
333
  "pipe",
@@ -346,7 +366,7 @@ function runScript$1(scriptsDirectory, scriptName, args, { timeoutMs = 3e4, json
346
366
  stderr = collect(stderr, chunk);
347
367
  });
348
368
  child.on("error", () => {
349
- rejectOnce(new HttpError(502, "script-launch-failed", "The CloudQ helper could not be started."));
369
+ rejectOnce(new HttpError(502, "script-launch-failed", "无法启动 Python 运行环境,请先安装 Python 3 后重试。"));
350
370
  });
351
371
  child.on("close", (code) => {
352
372
  if (settled) return;
@@ -405,6 +425,121 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
405
425
  function skillDirectory() {
406
426
  return resolve(__dirname, "../skills/cloudq");
407
427
  }
428
+ /** This plugin's own version, read from the installed package manifest. */
429
+ const PACKAGE_VERSION = (() => {
430
+ try {
431
+ return JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf8"))?.version ?? "0.0.0";
432
+ } catch {
433
+ return "0.0.0";
434
+ }
435
+ })();
436
+ /**
437
+ * Profile directory holding this plugin (`<profile>/node_modules/dsh-cloudq`).
438
+ * Both src/ (dev) and lib/ (packed) sit one level under the package root.
439
+ */
440
+ function profileDirectory() {
441
+ return resolve(__dirname, "../../..");
442
+ }
443
+ /** Fetch the latest published version from the npm registry (best effort). */
444
+ function fetchLatestPackageVersion() {
445
+ return new Promise((resolvePromise) => {
446
+ const request = get("https://registry.npmjs.org/dsh-cloudq/latest", { timeout: 8e3 }, (res) => {
447
+ if (res.statusCode !== 200) {
448
+ res.resume();
449
+ resolvePromise(null);
450
+ return;
451
+ }
452
+ let body = "";
453
+ res.on("data", (chunk) => {
454
+ body += chunk;
455
+ if (body.length > 65536) request.destroy();
456
+ });
457
+ res.on("end", () => {
458
+ try {
459
+ resolvePromise(JSON.parse(body)?.version ?? null);
460
+ } catch {
461
+ resolvePromise(null);
462
+ }
463
+ });
464
+ });
465
+ request.on("error", () => resolvePromise(null));
466
+ request.on("timeout", () => {
467
+ request.destroy();
468
+ resolvePromise(null);
469
+ });
470
+ });
471
+ }
472
+ let latestVersionCache = {
473
+ at: 0,
474
+ version: null
475
+ };
476
+ async function latestPackageVersion() {
477
+ if (latestVersionCache.version && Date.now() - latestVersionCache.at < 6e5) return latestVersionCache.version;
478
+ const version = await fetchLatestPackageVersion();
479
+ if (version) latestVersionCache = {
480
+ at: Date.now(),
481
+ version
482
+ };
483
+ return version;
484
+ }
485
+ /** Semver-ish compare: is `latest` strictly newer than `current`? */
486
+ function isNewerVersion(latest, current) {
487
+ const parse = (value) => String(value).split(".").map((part) => parseInt(part, 10) || 0);
488
+ const next = parse(latest);
489
+ const now = parse(current);
490
+ for (let index = 0; index < 3; index += 1) if (next[index] !== now[index]) return next[index] > now[index];
491
+ return false;
492
+ }
493
+ const UPDATE_TIMEOUT_MS = 12e4;
494
+ /** Install the latest published plugin version into the active profile. */
495
+ function runProfileUpdate() {
496
+ return new Promise((resolveRun, rejectRun) => {
497
+ const child = spawn("pnpm", [
498
+ "add",
499
+ "dsh-cloudq@latest",
500
+ "--registry=https://registry.npmjs.org/"
501
+ ], { cwd: profileDirectory() });
502
+ let output = "";
503
+ const collect = (chunk) => {
504
+ output += chunk;
505
+ if (output.length > 65536) child.kill();
506
+ };
507
+ child.stdout.on("data", collect);
508
+ child.stderr.on("data", collect);
509
+ const timer = setTimeout(() => {
510
+ child.kill();
511
+ rejectRun(new HttpError(504, "update-timeout", "更新超时,请检查网络后重试。"));
512
+ }, UPDATE_TIMEOUT_MS);
513
+ child.on("error", () => {
514
+ clearTimeout(timer);
515
+ rejectRun(new HttpError(502, "update-failed", "无法启动 pnpm,请在终端手动执行:dsh plugin --profile web add dsh-cloudq"));
516
+ });
517
+ child.on("close", (code) => {
518
+ clearTimeout(timer);
519
+ if (code === 0) resolveRun();
520
+ else rejectRun(new HttpError(502, "update-failed", `更新失败:${output.trim().slice(-200) || "pnpm 执行异常"}`));
521
+ });
522
+ });
523
+ }
524
+ /**
525
+ * Restart the DSH host after an update. A detached watcher respawns the same
526
+ * command line the moment this process exits; there is no supervisor, so the
527
+ * plugin exits itself once the watcher is armed.
528
+ */
529
+ function scheduleSelfRestart() {
530
+ if (process.platform === "win32") throw new HttpError(501, "restart-unsupported", "当前系统不支持自动重启,请手动重启 DSH 服务。");
531
+ const quote = (value) => `'${String(value).replaceAll("'", "'\\''")}'`;
532
+ const command = [
533
+ `while kill -0 ${process.pid} 2>/dev/null; do sleep 0.5; done`,
534
+ "sleep 1",
535
+ `cd ${quote(process.cwd())} && nohup ${quote(process.argv[0])} ${process.argv.slice(1).map(quote).join(" ")} >> /tmp/dsh-cloudq-restart.log 2>&1 &`
536
+ ].join("; ");
537
+ spawn("/bin/sh", ["-c", command], {
538
+ detached: true,
539
+ stdio: "ignore"
540
+ }).unref();
541
+ setTimeout(() => process.exit(0), 300).unref();
542
+ }
408
543
  /** Raw SKILL.md body. */
409
544
  function rawSkillContent() {
410
545
  return readFileSync(resolve(skillDirectory(), "SKILL.md"), "utf8");
@@ -676,6 +811,53 @@ function apply(ctx) {
676
811
  }
677
812
  }
678
813
  }));
814
+ disposers.push(ctx.webServer.register({
815
+ kind: "exact",
816
+ path: "/api/dsh-cloudq/version",
817
+ handler: async (request, response) => {
818
+ try {
819
+ assertSafeRequest(request, "GET");
820
+ const latest = await latestPackageVersion();
821
+ sendJson(response, 200, {
822
+ ok: true,
823
+ current: PACKAGE_VERSION,
824
+ latest,
825
+ outdated: latest !== null && isNewerVersion(latest, PACKAGE_VERSION)
826
+ });
827
+ } catch (error) {
828
+ sendError(response, error);
829
+ }
830
+ }
831
+ }));
832
+ disposers.push(ctx.webServer.register({
833
+ kind: "exact",
834
+ path: "/api/dsh-cloudq/update",
835
+ handler: async (request, response) => {
836
+ try {
837
+ assertSafeRequest(request, "POST");
838
+ await runProfileUpdate();
839
+ sendJson(response, 200, { ok: true });
840
+ } catch (error) {
841
+ sendError(response, error);
842
+ }
843
+ }
844
+ }));
845
+ disposers.push(ctx.webServer.register({
846
+ kind: "exact",
847
+ path: "/api/dsh-cloudq/restart",
848
+ handler: async (request, response) => {
849
+ try {
850
+ assertSafeRequest(request, "POST");
851
+ sendJson(response, 200, {
852
+ ok: true,
853
+ restarting: true
854
+ });
855
+ scheduleSelfRestart();
856
+ } catch (error) {
857
+ sendError(response, error);
858
+ }
859
+ }
860
+ }));
679
861
  disposers.push(ctx.webServer.register({
680
862
  kind: "exact",
681
863
  path: "/api/dsh-cloudq/credential/test",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cloudq",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "CloudQ integration for DeepSeek Harness with secure credential, workspace, and plugin-management surfaces",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -104,7 +104,7 @@ def find_available_role_name() -> tuple:
104
104
 
105
105
 
106
106
  def save_config(account_uin: str, role_arn: str, role_id: str = "",
107
- auto_created: bool = True):
107
+ auto_created: bool = True, role_name: str = ""):
108
108
  """保存配置到文件"""
109
109
  CONFIG_DIR.mkdir(parents=True, exist_ok=True)
110
110
 
@@ -114,15 +114,25 @@ def save_config(account_uin: str, role_arn: str, role_id: str = "",
114
114
  except OSError:
115
115
  pass
116
116
 
117
+ # 保留 DSH 插件写入的免密开关偏好,避免重建角色时被重置。
118
+ existing = {}
119
+ if CONFIG_FILE.exists():
120
+ try:
121
+ existing = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
122
+ except (json.JSONDecodeError, IOError):
123
+ existing = {}
124
+
117
125
  config = {
118
126
  "accountUin": account_uin,
119
- "roleName": ROLE_NAME,
127
+ "roleName": role_name or ROLE_NAME,
120
128
  "roleArn": role_arn,
121
129
  "roleId": role_id,
122
130
  "configuredAt": datetime.now(timezone.utc).isoformat(),
123
131
  "autoCreated": auto_created,
124
132
  "version": "1.0",
125
133
  }
134
+ if isinstance(existing.get("enabled"), bool):
135
+ config["enabled"] = existing["enabled"]
126
136
 
127
137
  CONFIG_FILE.write_text(
128
138
  json.dumps(config, indent=2, ensure_ascii=False),
@@ -153,6 +163,21 @@ def main():
153
163
  secret_id = os.environ.get("TENCENTCLOUD_SECRET_ID", "")
154
164
  secret_key = os.environ.get("TENCENTCLOUD_SECRET_KEY", "")
155
165
 
166
+ # 环境变量缺失时回退到凭证文件(与 login_url.py 一致),
167
+ # 使 DSH 插件等宿主可直接调用而无需注入环境变量。
168
+ if not secret_id or not secret_key:
169
+ try:
170
+ from credential_manager import get_credential
171
+ cred = get_credential()
172
+ secret_id = cred["secretId"]
173
+ secret_key = cred["secretKey"]
174
+ if cred.get("token"):
175
+ os.environ["TENCENTCLOUD_TOKEN"] = cred["token"]
176
+ except ImportError:
177
+ pass
178
+ except Exception:
179
+ pass
180
+
156
181
  if not secret_id or not secret_key:
157
182
  print(output_json(output_error(
158
183
  "MissingCredentials",
@@ -210,7 +235,7 @@ def main():
210
235
  attach_warnings.append(f"策略 {policy_name} 关联失败: {err_msg}")
211
236
  print(f"WARNING: {attach_warnings[-1]}", file=sys.stderr)
212
237
 
213
- save_config(account_uin, role_arn, role_id, auto_created=False)
238
+ save_config(account_uin, role_arn, role_id, auto_created=False, role_name=target_role)
214
239
  result = output_success(role_arn, account_uin, role_id)
215
240
  result["data"]["roleName"] = target_role
216
241
  if attach_warnings:
@@ -266,7 +291,7 @@ def main():
266
291
 
267
292
  # ============== 6. 保存配置 ==============
268
293
  role_arn = f"qcs::cam::uin/{account_uin}:roleName/{target_role}"
269
- save_config(account_uin, role_arn, role_id, auto_created=True)
294
+ save_config(account_uin, role_arn, role_id, auto_created=True, role_name=target_role)
270
295
 
271
296
  # ============== 7. 输出结果 ==============
272
297
  result = output_success(role_arn, account_uin, role_id)