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 +330 -155
- package/lib/index.js +188 -6
- package/package.json +1 -1
- package/skills/cloudq/scripts/create_role.py +29 -4
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
|
-
|
|
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
|
-
|
|
2042
|
-
let
|
|
2043
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
action.
|
|
2467
|
-
action.
|
|
2468
|
-
|
|
2469
|
-
action.
|
|
2470
|
-
action.
|
|
2471
|
-
action.
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
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
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
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
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
2740
|
-
card.href =
|
|
2898
|
+
if (consoleUrl) {
|
|
2899
|
+
card.href = consoleUrl;
|
|
2741
2900
|
card.target = "_blank";
|
|
2742
2901
|
card.rel = "noopener noreferrer";
|
|
2743
|
-
card.setAttribute("aria-label",
|
|
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
|
-
|
|
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
|
|
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", "
|
|
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
|
@@ -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)
|