dsh-showcase 0.1.2 → 0.1.4
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/CHANGELOG.md +35 -0
- package/README.md +83 -21
- package/README.zh-CN.md +3 -1
- package/package.json +17 -14
- package/plugin/client.js +39 -34
- package/plugin/dijiang-layout.js +161 -0
- package/plugin/dijiang-motion.js +101 -0
- package/plugin/dijiang-style.js +37 -0
- package/plugin/index.d.ts +20 -7
- package/plugin/index.js +72 -68
- package/plugin/layout-summary.d.ts +1 -0
- package/plugin/layout-summary.js +111 -23
- package/plugin/poster-html.js +117 -24
- package/plugin/project-io.d.ts +5 -0
- package/plugin/project-io.js +82 -0
- package/plugin/redaction.js +26 -0
- package/plugin/report-content.js +67 -0
- package/plugin/report-schema.js +18 -8
package/plugin/poster-html.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { renderDijiangDocument } from "./dijiang-layout.js";
|
|
2
|
+
import { findComparisonPair, formatReportDuration } from "./report-content.js";
|
|
3
|
+
|
|
4
|
+
const POSTER_RECORD_LIMIT = 20;
|
|
5
|
+
|
|
1
6
|
function escapeHtml(value) {
|
|
2
7
|
return String(value ?? "").replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]);
|
|
3
8
|
}
|
|
@@ -501,9 +506,80 @@ function manifestEmptyMarkup(kind, title, detail) {
|
|
|
501
506
|
].join("");
|
|
502
507
|
}
|
|
503
508
|
|
|
509
|
+
function fishDetailLedgerMarkup(data, text) {
|
|
510
|
+
const en = data.locale === "en";
|
|
511
|
+
const files = (Array.isArray(data.gitFiles) ? data.gitFiles : []).slice(0, POSTER_RECORD_LIMIT);
|
|
512
|
+
const totals = {
|
|
513
|
+
additions: data.additions == null ? files.reduce((sum, file) => sum + (Number(file.additions) || 0), 0) : Number(data.additions) || 0,
|
|
514
|
+
deletions: data.deletions == null ? files.reduce((sum, file) => sum + (Number(file.deletions) || 0), 0) : Number(data.deletions) || 0,
|
|
515
|
+
};
|
|
516
|
+
const screenshots = posterScreenshotItems(data);
|
|
517
|
+
const pair = findComparisonPair(screenshots);
|
|
518
|
+
const redactionDetails = Array.isArray(data.redactionDetails) ? data.redactionDetails : [];
|
|
519
|
+
const copy = en ? {
|
|
520
|
+
label: "Evidence detail ledger",
|
|
521
|
+
summary: "Delivery summary",
|
|
522
|
+
duration: "Duration",
|
|
523
|
+
delta: "Line changes",
|
|
524
|
+
captures: "Captures",
|
|
525
|
+
diff: "Line diff",
|
|
526
|
+
diffMissing: "No line diff retained in this report.",
|
|
527
|
+
diffBinary: "Binary file; no text diff.",
|
|
528
|
+
diffUntracked: "Untracked file; not included in the Git patch.",
|
|
529
|
+
diffOmitted: "Patch omitted by capture limits.",
|
|
530
|
+
diffTruncated: "Only part of the diff is retained.",
|
|
531
|
+
privacy: "Privacy review",
|
|
532
|
+
noPrivacy: "No redaction breakdown recorded.",
|
|
533
|
+
compare: "Before / after comparison",
|
|
534
|
+
before: "Before",
|
|
535
|
+
after: "After",
|
|
536
|
+
noCompare: "No matching before and after viewport pair.",
|
|
537
|
+
} : {
|
|
538
|
+
label: "证据明细台账",
|
|
539
|
+
summary: "交付摘要",
|
|
540
|
+
duration: "任务耗时",
|
|
541
|
+
delta: "增删总量",
|
|
542
|
+
captures: "截图记录",
|
|
543
|
+
diff: "逐行差异",
|
|
544
|
+
diffMissing: "报告未保留逐行 Diff。",
|
|
545
|
+
diffBinary: "二进制文件,不展示文本 Diff。",
|
|
546
|
+
diffUntracked: "未跟踪文件,尚未纳入 Git 差异。",
|
|
547
|
+
diffOmitted: "受采集大小或数量限制,此文件未保留 Diff。",
|
|
548
|
+
diffTruncated: "仅保留部分 Diff,内容已截断。",
|
|
549
|
+
privacy: "隐私审查",
|
|
550
|
+
noPrivacy: "未记录脱敏分类明细。",
|
|
551
|
+
compare: "改版前后对比",
|
|
552
|
+
before: "改版前",
|
|
553
|
+
after: "改版后",
|
|
554
|
+
noCompare: "尚无相同页面、相同视口的改版前后截图。",
|
|
555
|
+
};
|
|
556
|
+
const diffPanels = files.length
|
|
557
|
+
? files.map((file, index) => {
|
|
558
|
+
const unavailable = file.diffUnavailable === "binary"
|
|
559
|
+
? copy.diffBinary
|
|
560
|
+
: file.diffUnavailable === "untracked"
|
|
561
|
+
? copy.diffUntracked
|
|
562
|
+
: ["oversize", "limit"].includes(file.diffUnavailable)
|
|
563
|
+
? copy.diffOmitted
|
|
564
|
+
: copy.diffMissing;
|
|
565
|
+
const diff = file.diff === undefined
|
|
566
|
+
? '<p class="fish-detail-empty">' + escapeHtml(unavailable) + '</p>'
|
|
567
|
+
: '<pre class="fish-detail-diff">' + escapeHtml(file.diff) + '</pre>';
|
|
568
|
+
return '<details class="fish-detail-file"' + (index === 0 ? ' open' : '') + '><summary><span>' + escapeHtml(file.path) + '</span><b>+' + (Number(file.additions) || 0) + ' / -' + (Number(file.deletions) || 0) + '</b></summary>' + diff + (file.diffTruncated ? '<small>' + escapeHtml(copy.diffTruncated) + '</small>' : '') + '</details>';
|
|
569
|
+
}).join("")
|
|
570
|
+
: '<p class="fish-detail-empty">' + escapeHtml(copy.diffMissing) + '</p>';
|
|
571
|
+
const privacy = redactionDetails.length
|
|
572
|
+
? redactionDetails.map((item) => '<li><span>' + escapeHtml(item.name) + '</span><b>' + (Number(item.count) || 0) + '</b></li>').join("")
|
|
573
|
+
: '<li class="fish-detail-empty">' + escapeHtml(copy.noPrivacy) + '</li>';
|
|
574
|
+
const comparison = pair
|
|
575
|
+
? '<div class="fish-detail-comparison" data-fish-comparison><header><b>' + escapeHtml(copy.compare) + '</b><small>' + escapeHtml(pair.after.label || pair.before.label || "") + '</small></header><div><figure><img src="data:' + escapeHtml(pair.before.mimeType) + ';base64,' + pair.before.image + '" alt="' + escapeHtml(copy.before) + '"><figcaption>' + escapeHtml(copy.before) + '</figcaption></figure><figure><img src="data:' + escapeHtml(pair.after.mimeType) + ';base64,' + pair.after.image + '" alt="' + escapeHtml(copy.after) + '"><figcaption>' + escapeHtml(copy.after) + '</figcaption></figure></div></div>'
|
|
576
|
+
: '<div class="fish-detail-comparison fish-detail-comparison--empty"><header><b>' + escapeHtml(copy.compare) + '</b></header><p>' + escapeHtml(copy.noCompare) + '</p></div>';
|
|
577
|
+
return '<section class="fish-detail-ledger" aria-label="' + escapeHtml(copy.label) + '"><header class="fish-detail-ledger__head"><span>' + escapeHtml(copy.label) + '</span><b>' + escapeHtml(copy.summary) + '</b></header><dl class="fish-detail-overview"><div><dt>' + escapeHtml(copy.duration) + '</dt><dd>' + escapeHtml(formatReportDuration(data.durationMs, data.locale)) + '</dd></div><div><dt>' + escapeHtml(copy.delta) + '</dt><dd><strong>+' + totals.additions + '</strong><i>−' + totals.deletions + '</i></dd></div><div><dt>' + escapeHtml(copy.captures) + '</dt><dd>' + screenshots.length + '</dd></div></dl><div class="fish-detail-columns"><section><h3>' + escapeHtml(copy.diff) + '</h3>' + diffPanels + '</section><section><h3>' + escapeHtml(copy.privacy) + '</h3><ul class="fish-detail-privacy">' + privacy + '</ul></section></div>' + comparison + '</section>';
|
|
578
|
+
}
|
|
579
|
+
|
|
504
580
|
function deliveryManifestMarkup(data, text) {
|
|
505
581
|
const status = statusPresentation(data, text);
|
|
506
|
-
const rawGitFiles = (Array.isArray(data.gitFiles) ? data.gitFiles : []).slice(0,
|
|
582
|
+
const rawGitFiles = (Array.isArray(data.gitFiles) ? data.gitFiles : []).slice(0, POSTER_RECORD_LIMIT);
|
|
507
583
|
const untrackedDirectories = rawGitFiles.filter((file) => String(file.path ?? "").endsWith("/") && !(Number(file.additions) || Number(file.deletions)));
|
|
508
584
|
const changedGitFiles = rawGitFiles.filter((file) => !untrackedDirectories.includes(file));
|
|
509
585
|
const inferredGitState = String(data.gitRange ?? "").includes("NO_GIT") ? "unavailable" : rawGitFiles.length ? "changed" : "clean";
|
|
@@ -523,15 +599,19 @@ function deliveryManifestMarkup(data, text) {
|
|
|
523
599
|
: changedGitItems || manifestEmptyMarkup("git-details-missing", text.gitChangedMissingTitle, text.gitChangedMissingDetail);
|
|
524
600
|
const gitMeta = gitState === "unavailable" ? text.gitUnavailableMeta : String(data.gitRange ?? "");
|
|
525
601
|
|
|
526
|
-
const rawReceipts = (Array.isArray(data.testReceipts) ? data.testReceipts : []).slice(0,
|
|
602
|
+
const rawReceipts = (Array.isArray(data.testReceipts) ? data.testReceipts : []).slice(0, POSTER_RECORD_LIMIT);
|
|
527
603
|
const inferredTestState = rawReceipts.length ? "configured" : "unconfigured";
|
|
528
604
|
const testState = ["unconfigured", "failed", "configured"].includes(data.testState) ? data.testState : inferredTestState;
|
|
529
605
|
const receiptItems = rawReceipts.map((receipt) => {
|
|
530
606
|
const receiptState = receiptPresentation(receipt, text);
|
|
531
607
|
const exitCode = Number(receipt.exitCode) || 0;
|
|
532
608
|
const ariaLabel = String(receipt.command ?? "") + ": " + receiptState.label + ", " + text.exit + " " + exitCode;
|
|
609
|
+
const output = String(receipt.output ?? "");
|
|
610
|
+
const outputDetails = output
|
|
611
|
+
? '<details class="manifest-receipt-output"><summary>' + escapeHtml(data.locale === "en" ? "View output" : "查看输出") + '</summary><pre>' + escapeHtml(output) + '</pre>' + (receipt.outputTruncated ? '<small>' + escapeHtml(data.locale === "en" ? "Output truncated." : "输出已截断。") + '</small>' : '') + '</details>'
|
|
612
|
+
: '';
|
|
533
613
|
return [
|
|
534
|
-
'<li data-status="', receiptState.key, '" aria-label="', escapeHtml(ariaLabel), '"><span>', escapeHtml(receipt.command), '</span><b><span aria-hidden="true">', escapeHtml(receiptState.symbol), '</span> ', escapeHtml(receiptState.label), ' · ', escapeHtml(receipt.duration), ' · ', escapeHtml(text.exit), ' ', exitCode, '</b
|
|
614
|
+
'<li data-status="', receiptState.key, '" aria-label="', escapeHtml(ariaLabel), '"><span>', escapeHtml(receipt.command), '</span><b><span aria-hidden="true">', escapeHtml(receiptState.symbol), '</span> ', escapeHtml(receiptState.label), ' · ', escapeHtml(receipt.duration), ' · ', escapeHtml(text.exit), ' ', exitCode, '</b>', outputDetails, '</li>',
|
|
535
615
|
].join("");
|
|
536
616
|
}).join("");
|
|
537
617
|
const receipts = receiptItems || (testState === "unconfigured"
|
|
@@ -575,7 +655,8 @@ function deliveryManifestMarkup(data, text) {
|
|
|
575
655
|
'<div class="manifest-grid"><article class="manifest-block" data-block-state="', gitState, '"><div class="manifest-block__head"><span>', escapeHtml(text.manifestChanges), '</span><small>', escapeHtml(gitMeta), '</small></div><ul data-empty="', changedGitItems ? 'false' : 'true', '">', gitItems, '</ul></article>',
|
|
576
656
|
'<article class="manifest-block" data-block-state="', testState, '"><div class="manifest-block__head"><span>', escapeHtml(text.manifestReceipts), '</span><small>', escapeHtml(testMeta), '</small></div><ul data-empty="', rawReceipts.length ? 'false' : 'true', '">', receipts, '</ul></article></div>',
|
|
577
657
|
'<div class="manifest-scope"><small>', escapeHtml(text.manifestScope), '</small><div>', scope, '</div></div>',
|
|
578
|
-
'<footer class="manifest-output" data-status="', status.key, '" data-output-state="', outputState, '"><div class="manifest-output__meta"><div class="manifest-output__label"><span>', escapeHtml(text.manifestOutput), '</span><small data-status="', status.key, '" aria-label="', escapeHtml(statusAriaLabel(data.locale, outputLine ? status.output : text.outputsMissingTitle)), '">', escapeHtml(outputLine ? status.output : text.outputsMissingTitle), '</small></div><small class="manifest-output__privacy">', Number(data.redactionCount) || 0, ' ', escapeHtml(text.manifestRedactions), ' / ', escapeHtml(text.manifestLocal), '</small></div><div class="manifest-output__path">', outputSteps, '</div></footer
|
|
658
|
+
'<footer class="manifest-output" data-status="', status.key, '" data-output-state="', outputState, '"><div class="manifest-output__meta"><div class="manifest-output__label"><span>', escapeHtml(text.manifestOutput), '</span><small data-status="', status.key, '" aria-label="', escapeHtml(statusAriaLabel(data.locale, outputLine ? status.output : text.outputsMissingTitle)), '">', escapeHtml(outputLine ? status.output : text.outputsMissingTitle), '</small></div><small class="manifest-output__privacy">', Number(data.redactionCount) || 0, ' ', escapeHtml(text.manifestRedactions), ' / ', escapeHtml(text.manifestLocal), '</small></div><div class="manifest-output__path">', outputSteps, '</div></footer>',
|
|
659
|
+
fishDetailLedgerMarkup(data, text), '</section>',
|
|
579
660
|
].join("");
|
|
580
661
|
}
|
|
581
662
|
|
|
@@ -780,7 +861,7 @@ function dijiangLoaderMarkup(text, stages, locale, projectName, statusKey = "com
|
|
|
780
861
|
'<div class="field-loader-stages" aria-hidden="true">', stageItems, '</div><span class="field-loader-anchor" aria-hidden="true"></span>',
|
|
781
862
|
'<span class="field-loader-release" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></span></footer></section>',
|
|
782
863
|
'<div class="field-loader-scan" aria-hidden="true"></div>',
|
|
783
|
-
'<script>(function(){var node=document.querySelector(".loader");if(!node)return;var done=false;var root=document.documentElement;root.dataset.loaderReady="false";node.setAttribute("aria-busy","true");var started=performance.now();var duration=2750;var fill=node.querySelector("[data-loader-fill]");var percent=node.querySelector("[data-loader-percent]");var status=node.querySelector("[data-loader-status]");var stages=[].slice.call(node.querySelectorAll("[data-loader-stage]"));var locale=node.getAttribute("data-loader-locale");var outcome=node.getAttribute("data-loader-outcome")||"completed";var finalMessage=locale==="en"?({completed:"DELIVERY READY",partial:"REVIEW REQUIRED",failed:"DELIVERY BLOCKED"})[outcome]:({completed:"交付链路就绪",partial:"等待复核",failed:"交付链路阻断"})[outcome];var messages=locale==="en"?["LINK BEACON","VERIFY INDEX","
|
|
864
|
+
'<script>(function(){var node=document.querySelector(".loader");if(!node)return;var done=false;var root=document.documentElement;root.dataset.loaderReady="false";node.setAttribute("aria-busy","true");var started=performance.now();var duration=2750;var fill=node.querySelector("[data-loader-fill]");var percent=node.querySelector("[data-loader-percent]");var status=node.querySelector("[data-loader-status]");var stages=[].slice.call(node.querySelectorAll("[data-loader-stage]"));var locale=node.getAttribute("data-loader-locale");var outcome=node.getAttribute("data-loader-outcome")||"completed";var finalMessage=locale==="en"?({completed:"DELIVERY READY",partial:"REVIEW REQUIRED",failed:"DELIVERY BLOCKED"})[outcome]:({completed:"交付链路就绪",partial:"等待复核",failed:"交付链路阻断"})[outcome];var messages=locale==="en"?["LINK BEACON","VERIFY INDEX","PREPARE RECEIPTS","SYNC EVIDENCE",finalMessage]:["接入帝江号信标","校验证据索引","整理验证回执","同步主题证据",finalMessage];function tick(now){if(done)return;var raw=Math.min(1,(now-started)/duration);var messageIndex=raw<.2?0:raw<.42?1:raw<.65?2:raw<.97?3:4;if(fill)fill.style.transform="scaleX("+raw+")";if(percent)percent.textContent=String(Math.round(raw*100)).padStart(2,"0")+"%";if(status)status.textContent=messages[messageIndex];stages.forEach(function(stage,index){var start=index*.18;var finish=index===4?.96:(index+1)*.18;stage.classList.toggle("is-live",raw>=start);stage.classList.toggle("is-done",raw>=finish)});if(raw<1)requestAnimationFrame(tick)}function remove(){if(done)return;done=true;node.dataset.loaderState="complete";node.setAttribute("aria-busy","false");root.dataset.loaderReady="true";node.remove()}function fallbackDelay(){var style=getComputedStyle(node);var animationDuration=parseFloat(style.animationDuration)||0;var animationDelay=parseFloat(style.animationDelay)||0;return Math.max(5000,Math.ceil((animationDuration+animationDelay)*1000)+600)}requestAnimationFrame(tick);function onExit(event){if(event.target===node){node.removeEventListener("animationend",onExit);remove()}}node.addEventListener("animationend",onExit);setTimeout(remove,fallbackDelay())})()</script>',
|
|
784
865
|
'</div>',
|
|
785
866
|
].join("");
|
|
786
867
|
}
|
|
@@ -939,23 +1020,11 @@ const dijiangLoaderCompactCss = `
|
|
|
939
1020
|
function createDijiangPoster(data) {
|
|
940
1021
|
const text = posterText(data.locale);
|
|
941
1022
|
const status = statusPresentation(data, text);
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
:
|
|
946
|
-
|
|
947
|
-
'<!doctype html><html lang="', data.locale, '"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>', escapeHtml(text.title), '</title><style>', sharedCss, evidenceIntegrityCss, posterCss, dijiangCompactCss, surfaceCss, dijiangTopologyCss, dijiangMaterialCss, dijiangLoaderCompactCss, manifestStateCss, dijiangReportContentCss, statusCss, '</style></head><body>', dijiangLoaderMarkup(text, data.stages, data.locale, data.projectName, status.key),
|
|
948
|
-
'<article class="poster dijiang-poster" data-theme="frontier-signal" data-status="', status.key, '"><div class="field-topband"></div><div class="field-sheet"></div>', dijiangContoursMarkup("field-contours"),
|
|
949
|
-
dijiangSlashedWordMarkup("field-topmark"), '<div class="field-safety-slab" aria-hidden="true"></div>', dijiangBlueprintMarkup(), '<div class="field-copy-panel" aria-hidden="true"></div>',
|
|
950
|
-
'<div class="mast"><span>DSH / ', escapeHtml(text.title), '</span><span>0017 / ', escapeHtml(text.local), '</span></div>',
|
|
951
|
-
'<div class="copy"><div class="kicker">', escapeHtml(text.fieldTheme), ' / ', escapeHtml(status.fieldKicker), '</div><h1 class="title">', escapeHtml(data.projectName), '</h1><p class="task">', escapeHtml(data.task), '</p><div class="verified" data-status="', status.key, '" role="status" aria-label="', escapeHtml(statusAriaLabel(data.locale, status.badge)), '"><span aria-hidden="true">', escapeHtml(status.symbol), '</span> ', escapeHtml(status.badge), '</div><div class="field-calibration" aria-hidden="true"><i></i><i></i><i></i></div></div>', dijiangReadoutMarkup(data, status),
|
|
952
|
-
'<div class="stamp" data-status="', status.key, '" aria-label="', escapeHtml(statusAriaLabel(data.locale, status.deliverable)), '">', escapeHtml(status.deliverable), '</div><div class="metrics">', posterMetricsMarkup(data, text), '</div>',
|
|
953
|
-
'<div class="rail">', stageMarkup(data.stages), '</div><div class="footer mono">', escapeHtml(posterFooterText(data, text)), '</div></article>',
|
|
954
|
-
currentRouteMarkup(data.stages, text),
|
|
955
|
-
deliveryManifestMarkup(data, text),
|
|
956
|
-
evidencePageMarkup(data, text),
|
|
957
|
-
'</body></html>',
|
|
958
|
-
].join("");
|
|
1023
|
+
return renderDijiangDocument(data, text, status, {
|
|
1024
|
+
instrument: dijiangLoaderInstrumentMarkup(),
|
|
1025
|
+
metrics: posterMetricsMarkup(data, text),
|
|
1026
|
+
integrity: evidenceIntegrityMarkup(data, text),
|
|
1027
|
+
});
|
|
959
1028
|
}
|
|
960
1029
|
|
|
961
1030
|
function createFieldPoster(data) {
|
|
@@ -987,6 +1056,9 @@ function createFieldPoster(data) {
|
|
|
987
1056
|
|
|
988
1057
|
const fishManifestHardeningCss = "@media(max-width:35rem){.delivery-manifest{min-width:0;overflow:hidden}.delivery-manifest .manifest-block li{align-items:flex-start;flex-wrap:wrap;gap:.2rem .45rem}.delivery-manifest .manifest-block li span{flex:1 1 9rem;max-width:100%;overflow:hidden;overflow-wrap:anywhere;white-space:normal;text-overflow:clip}.delivery-manifest .manifest-block li b{flex:0 1 auto;max-width:100%;white-space:normal;overflow-wrap:anywhere}.delivery-manifest .manifest-output,.delivery-manifest .manifest-output__meta,.delivery-manifest .manifest-output__label,.delivery-manifest .manifest-output__path{width:100%;min-width:0;max-width:100%}.delivery-manifest .manifest-output{overflow:hidden}.delivery-manifest .manifest-output__meta{display:grid;grid-template-columns:minmax(0,1fr);gap:.3rem}.delivery-manifest .manifest-output__label{display:grid;grid-template-columns:minmax(0,1fr);gap:.15rem}.delivery-manifest .manifest-output__label span,.delivery-manifest .manifest-output__label small,.delivery-manifest .manifest-output__privacy{min-width:0;max-width:100%;overflow:hidden;overflow-wrap:anywhere;white-space:normal}.delivery-manifest .manifest-output__path{display:grid!important;grid-template-columns:minmax(0,1fr)!important;gap:.4rem}.delivery-manifest .manifest-output__path span{display:grid;grid-template-columns:auto minmax(0,1fr);width:100%;min-width:0;max-width:100%;padding:0;overflow:visible}.delivery-manifest .manifest-output__path strong{min-width:0;max-width:100%;overflow:hidden;overflow-wrap:anywhere;white-space:normal;text-overflow:clip}.delivery-manifest .manifest-output__privacy{text-align:left;line-height:1.35}}";
|
|
989
1058
|
|
|
1059
|
+
const fishReceiptCss = ".manifest-receipt-output{flex:1 1 100%;min-width:0;margin-top:.2rem}.manifest-receipt-output summary{cursor:pointer;list-style:none;color:#0757cf;font:800 .7rem/1.35 'Cascadia Mono','Microsoft YaHei UI',monospace}.manifest-receipt-output summary::-webkit-details-marker{display:none}.manifest-receipt-output summary:before{content:'+';display:inline-block;width:1.2em}.manifest-receipt-output[open] summary:before{content:'−'}.manifest-receipt-output pre{max-height:12rem;margin:.35rem 0 0;padding:.55rem;background:rgba(7,87,207,.08);color:#10245c;white-space:pre-wrap;overflow:auto;overflow-wrap:anywhere;font:700 .7rem/1.45 'Cascadia Mono','Microsoft YaHei UI',monospace}.manifest-receipt-output>small{display:block;margin-top:.25rem;color:#2865ad;font:700 .65rem/1.3 'Cascadia Mono','Microsoft YaHei UI',monospace}";
|
|
1060
|
+
const fishDetailCss = ".fish-detail-ledger{position:relative;z-index:1;margin:clamp(.7rem,1vw,1rem) 0 0;padding:clamp(.75rem,1.1vw,1rem);color:#10245c;background:rgba(255,255,255,.76);border-top:.14rem solid #0757cf}.fish-detail-ledger__head{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;padding-bottom:.5rem;border-bottom:.12rem solid #8bd8f8}.fish-detail-ledger__head span{color:#0757cf;font:900 .75rem/1.2 'Cascadia Mono','Microsoft YaHei UI',monospace;text-transform:uppercase}.fish-detail-ledger__head b{font-size:clamp(1rem,1.2vw,1.25rem)}.fish-detail-overview{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:.5rem;margin:.7rem 0 0}.fish-detail-overview div{min-width:0;padding:.55rem .65rem;background:#eaf7ff;border-left:.2rem solid #48bfe9}.fish-detail-overview dt{color:#2865ad;font:800 .65rem/1.2 'Cascadia Mono','Microsoft YaHei UI',monospace}.fish-detail-overview dd{margin:.2rem 0 0;font:900 clamp(.9rem,1vw,1.1rem)/1.2 'Bahnschrift','Microsoft YaHei UI',sans-serif;overflow-wrap:anywhere}.fish-detail-overview dd strong{color:#087f77}.fish-detail-overview dd i{margin-left:.45rem;color:#d44a62;font-style:normal}.fish-detail-columns{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(10rem,.65fr);gap:clamp(.75rem,1.2vw,1.2rem);margin-top:1rem}.fish-detail-columns>section{min-width:0}.fish-detail-columns h3{margin:0 0 .4rem;color:#0757cf;font:900 .85rem/1.2 'Cascadia Mono','Microsoft YaHei UI',monospace}.fish-detail-file{border-top:1px solid rgba(7,87,207,.2)}.fish-detail-file:last-child{border-bottom:1px solid rgba(7,87,207,.2)}.fish-detail-file summary{display:flex;align-items:baseline;justify-content:space-between;gap:.7rem;min-width:0;padding:.42rem 0;cursor:pointer;color:#1b3470;font:800 .76rem/1.25 'Cascadia Mono','Microsoft YaHei UI',monospace}.fish-detail-file summary span{min-width:0;overflow-wrap:anywhere}.fish-detail-file summary b{flex:0 0 auto;color:#0757cf;white-space:nowrap}.fish-detail-diff{max-height:12rem;margin:0 0 .4rem;padding:.55rem;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;color:#10245c;background:#eef7ff;border-left:.2rem solid #48bfe9;font:700 .68rem/1.4 'Cascadia Mono','Microsoft YaHei UI',monospace}.fish-detail-file>small{display:block;margin:0 0 .4rem;color:#2865ad;font:700 .65rem/1.3 'Cascadia Mono','Microsoft YaHei UI',monospace}.fish-detail-empty{margin:.35rem 0;color:#2865ad;font:700 .7rem/1.4 'Cascadia Mono','Microsoft YaHei UI',monospace}.fish-detail-privacy{display:grid;gap:.35rem;margin:0;padding:0;list-style:none}.fish-detail-privacy li{display:flex;justify-content:space-between;gap:.6rem;padding:.4rem .5rem;background:#eaf7ff;border-bottom:1px solid rgba(7,87,207,.17);font:800 .72rem/1.25 'Cascadia Mono','Microsoft YaHei UI',monospace}.fish-detail-privacy b{color:#087f77}.fish-detail-comparison{margin-top:1rem;padding-top:.7rem;border-top:.12rem solid #8bd8f8}.fish-detail-comparison header{display:flex;align-items:baseline;justify-content:space-between;gap:.7rem}.fish-detail-comparison header b{color:#0757cf;font:900 .85rem/1.2 'Cascadia Mono','Microsoft YaHei UI',monospace}.fish-detail-comparison header small{color:#2865ad;font:700 .68rem/1.2 'Cascadia Mono','Microsoft YaHei UI',monospace;overflow-wrap:anywhere}.fish-detail-comparison>div{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.6rem;margin-top:.55rem}.fish-detail-comparison figure{min-width:0;margin:0}.fish-detail-comparison img{display:block;width:100%;aspect-ratio:16/9;object-fit:cover;border:1px solid #8bd8f8;background:#eaf7ff}.fish-detail-comparison figcaption{margin-top:.25rem;color:#2865ad;font:800 .66rem/1.2 'Cascadia Mono','Microsoft YaHei UI',monospace}.fish-detail-comparison--empty p{margin:.5rem 0 0;color:#2865ad;font:700 .7rem/1.35 'Cascadia Mono','Microsoft YaHei UI',monospace}@media(max-width:56.249rem){.fish-detail-overview{grid-template-columns:1fr}.fish-detail-columns{grid-template-columns:1fr;gap:1rem}.fish-detail-comparison>div{grid-template-columns:1fr}.fish-detail-ledger__head{align-items:flex-start;flex-direction:column;gap:.25rem}}";
|
|
1061
|
+
|
|
990
1062
|
const manifestStateCss = [
|
|
991
1063
|
".manifest-block ul[data-empty='true']{align-content:stretch}.manifest-block:last-child ul[data-empty='true']:after{display:none}.manifest-block .manifest-empty{display:block;min-height:4.25rem;padding:clamp(.55rem,.75vw,.75rem);border:.08rem solid rgba(7,87,207,.2);background:rgba(255,255,255,.42)}.manifest-block .manifest-empty>span{display:grid;gap:.22rem;max-width:100%;overflow:visible;white-space:normal}.manifest-block .manifest-empty strong{color:#0757cf;font-family:'Bahnschrift','Microsoft YaHei UI',sans-serif;font-size:clamp(.875rem,.98vw,1rem);font-weight:900;line-height:1.2}.manifest-block .manifest-empty small{color:#2865ad;font-size:clamp(.6875rem,.76vw,.8125rem);font-weight:750;line-height:1.35;white-space:normal}.manifest-empty[data-empty-state='git-unavailable']{border-color:rgba(8,127,119,.35);background:rgba(220,250,247,.54)}.manifest-empty[data-empty-state='tests-unconfigured']{border-color:rgba(255,210,74,.58);background:rgba(255,246,207,.6)}",
|
|
992
1064
|
".manifest-output[data-output-state='empty'] .manifest-output__path{grid-template-columns:1fr}.manifest-output__empty{display:grid;gap:.2rem;min-width:0;padding:.55rem .65rem;border:.08rem solid rgba(7,87,207,.2);background:rgba(234,247,255,.74)}.manifest-output__empty strong{color:#0757cf;font:900 clamp(.8125rem,.9vw,.9375rem) 'Bahnschrift','Microsoft YaHei UI',sans-serif;white-space:normal}.manifest-output__empty small{color:#2865ad;font:750 clamp(.6875rem,.76vw,.8125rem)/1.35 'Cascadia Mono','Microsoft YaHei UI',monospace;white-space:normal}",
|
|
@@ -1033,7 +1105,7 @@ function createFishPoster(data) {
|
|
|
1033
1105
|
"@keyframes current-drift{to{transform:translate3d(7vw,1.2vh,0) scale(1.03)}}@keyframes ripple-spin{to{transform:rotate(360deg)}}@keyframes portrait-tide{to{transform:translate3d(1.2vw,-.55vh,0) scale(1.015);opacity:.76}}@keyframes portrait-current{to{transform:translate3d(7%,0,0)}}@keyframes whale-drift{to{translate:7vw -2vh;rotate:4deg}}@keyframes light-sweep{from{transform:translate3d(-6vw,0,0) rotate(16deg)}to{transform:translate3d(10vw,0,0) rotate(16deg)}}@keyframes bubble-rise{0%{opacity:0;translate:0 0;scale:.65}12%{opacity:.8}78%{opacity:.38}100%{opacity:0;translate:2vw -108vh;scale:1.35}}@keyframes sparkle{50%{transform:scale(1.35) rotate(45deg);opacity:.55}}@keyframes route-swell{to{stroke-dashoffset:-176}}@keyframes evidence-current{to{transform:translate3d(12%,-4%,0) scale(1.06)}}@keyframes evidence-scan{to{transform:translateX(78vw) skewX(-10deg)}}@media(max-height:48rem) and (min-width:56.25rem){.poster-layout{padding-block:1.5rem .8rem;grid-template-rows:auto auto auto minmax(10rem,1fr) auto;row-gap:.42rem}.title{font-size:clamp(3.25rem,4.9vw,4.35rem)}.task{font-size:clamp(1rem,1.25vw,1.1875rem);line-height:1.3}.verified{margin-top:.62rem;padding:.38rem .65rem}.portrait-field{min-height:14rem}.current-route{min-height:3.7rem}.route-proofs{height:calc(100% - 3.3rem)}.metric{padding:.45rem .7rem}.route-evidence{padding:.7rem .85rem}}@media(max-width:56.249rem){.poster{height:auto;min-height:0}.poster-layout{width:auto;height:auto;margin:0;padding:4.75rem 1rem 1.25rem;grid-template-columns:1fr;grid-template-areas:'copy' 'portrait' 'route' 'metrics' 'evidence' 'footer';grid-template-rows:auto 15rem auto auto auto auto;row-gap:.85rem}.copy{max-width:100%}.title{font-size:clamp(3rem,12vw,4.25rem)}.task{font-size:1.0625rem}.portrait-field{min-height:0;height:15rem}.portrait-field:before{inset:4% 0 0 10%}.portrait-field .art{inset:0;width:100%;height:100%;object-position:50% 0%}.portrait-field:after{bottom:14%}.current-route{min-height:12.25rem;padding:.2rem 0 0}.current-route header{align-items:flex-start;flex-direction:column;gap:.2rem}.route-swell{display:none}.route-nodes{min-height:0;grid-template-columns:1fr;gap:.25rem;padding:.65rem 0 .1rem 1.1rem;border-left:.18rem solid #48bfe9}.route-nodes span,.route-nodes span:nth-child(2),.route-nodes span:nth-child(4){display:grid;grid-template-columns:auto 1fr;align-items:center;column-gap:.65rem;transform:none}.route-nodes em{font-size:1rem}.metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:.45rem}.metric{min-width:0;display:grid;align-content:center;gap:.15rem;padding:.55rem .6rem}.metric b{font-size:1.6rem}.metric small{font-size:.8125rem;line-height:1.15}.route-evidence{min-height:0;padding:.85rem}.route-evidence header{align-items:flex-start;flex-direction:column;gap:.3rem}.route-proofs{height:auto;display:grid;grid-template-columns:1fr;gap:.8rem;padding-top:.8rem}.route-proof{min-height:0}.route-proof__frame{aspect-ratio:16/10}.route-proof:nth-child(2) .route-proof__frame{aspect-ratio:4/3}.route-proof:nth-child(3) .route-proof__frame{aspect-ratio:10/11}.route-proof:nth-child(2) .route-proof__frame,.route-proof:nth-child(3) .route-proof__frame{transform:none}.footer{padding-top:.15rem}.whale-one{right:13%;top:37%;width:17vw}.whale-two{right:4%;top:64%;width:13vw}.whale-three{display:none}}@media(min-width:35rem) and (max-width:56.249rem){.poster-layout{padding-inline:2rem;grid-template-rows:auto 22rem auto auto auto auto}.portrait-field{height:22rem}.current-route{min-height:12rem}.route-proofs{grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr)}.route-proof:first-child{grid-column:1/-1}.route-proof:first-child .route-proof__frame{aspect-ratio:16/8}.route-proof:nth-child(2) .route-proof__frame{aspect-ratio:4/3}.route-proof:nth-child(3) .route-proof__frame{aspect-ratio:4/5}}@media(prefers-reduced-motion:reduce){.loader{animation-duration:.01ms}.current,.ripple,.light,.whale,.spark,.bubbles i,.portrait-field:after,.portrait-field .art,.route-evidence:after{animation:current-breathe 7s ease-in-out infinite alternate}.route-swell path{animation:none;stroke-dashoffset:0}@keyframes current-breathe{to{opacity:.72}}}",
|
|
1034
1106
|
].join("");
|
|
1035
1107
|
return [
|
|
1036
|
-
'<!doctype html><html lang="', data.locale, '"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>', escapeHtml(text.title), '</title><style>', sharedCss, evidenceIntegrityCss, css, fishManifestHardeningCss, manifestStateCss, fishGlassCss, fishVisualHardeningCss, statusCss, '</style></head><body>', loadingMarkup(text, "fish", status.key, data.projectName),
|
|
1108
|
+
'<!doctype html><html lang="', data.locale, '"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>', escapeHtml(text.title), '</title><style>', sharedCss, evidenceIntegrityCss, css, fishManifestHardeningCss, fishReceiptCss, fishDetailCss, manifestStateCss, fishGlassCss, fishVisualHardeningCss, statusCss, '</style></head><body>', loadingMarkup(text, "fish", status.key, data.projectName),
|
|
1037
1109
|
'<article class="poster" data-theme="blue-big-fish" data-status="', status.key, '"><div class="current current-one"></div><div class="current current-two"></div><div class="current current-three"></div><div class="ripple ripple-one"></div><div class="ripple ripple-two"></div><div class="light light-one"></div><div class="light light-two"></div>', bubbleMarkup(), whaleSchoolMarkup(), '<i class="spark s1"></i><i class="spark s2"></i><i class="spark s3"></i>',
|
|
1038
1110
|
'<div class="poster-layout"><section class="copy"><div class="kicker">', escapeHtml(text.fishTheme), ' / ', escapeHtml(status.fishKicker), '</div><h1 class="title">', escapeHtml(data.projectName), '</h1><p class="task">', escapeHtml(data.task), '</p><div class="verified" data-status="', status.key, '" role="status" aria-label="', escapeHtml(statusAriaLabel(data.locale, status.badge)), '"><span aria-hidden="true">', escapeHtml(status.symbol), '</span> ', escapeHtml(status.summary), '</div></section>',
|
|
1039
1111
|
'<aside class="portrait-field" aria-hidden="true"><img class="art" src="data:image/webp;base64,', data.image, '" alt=""></aside>',
|
|
@@ -1045,5 +1117,26 @@ function createFishPoster(data) {
|
|
|
1045
1117
|
|
|
1046
1118
|
export function createStyledPosterHtml(data) {
|
|
1047
1119
|
const normalizedData = normalizePosterData(data);
|
|
1048
|
-
|
|
1120
|
+
if (normalizedData.theme !== "blue-big-fish") return createDijiangPoster(normalizedData);
|
|
1121
|
+
const html = createFishPoster(normalizedData);
|
|
1122
|
+
const warnings = Array.isArray(data?.freshnessWarnings) ? data.freshnessWarnings : [];
|
|
1123
|
+
const notice = warnings.length ? `<aside class="evidence-warnings" role="status"><strong>${normalizedData.locale === "en" ? "Evidence needs review" : "证据需要复核"}</strong><ul>${warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join("")}</ul></aside>` : "";
|
|
1124
|
+
return html.replace("</style>", `${responsiveEvidenceCss}</style>`).replace('<article class="poster', `${notice}<article class="poster`);
|
|
1049
1125
|
}
|
|
1126
|
+
|
|
1127
|
+
const responsiveEvidenceCss = `
|
|
1128
|
+
.evidence-warnings{position:relative;padding:1rem 5%;background:#fff2cc;color:#402f0a;border-bottom:2px solid #c79708;font:1rem/1.5 sans-serif;overflow-wrap:anywhere}.evidence-warnings ul{margin:.4rem 0 0;padding-left:1.3rem}
|
|
1129
|
+
.poster{height:auto;min-height:min(100svh,72rem)}
|
|
1130
|
+
.poster .title{font-size:3.5rem;line-height:1.04;overflow-wrap:anywhere}.poster .task{font-size:1.125rem;line-height:1.5}
|
|
1131
|
+
.poster-layout{height:auto;min-height:inherit;grid-template-rows:auto auto auto auto auto}
|
|
1132
|
+
.poster-layout .current-route{min-height:7rem;height:auto;overflow:visible}.poster-layout .route-nodes{min-height:4.5rem}
|
|
1133
|
+
.delivery-manifest{grid-template-rows:auto auto auto auto}.manifest-block:last-child ul:after{content:none}
|
|
1134
|
+
.poster-layout .delivery-manifest{overflow:hidden}.poster-layout .manifest-grid{min-height:0}
|
|
1135
|
+
.poster-layout .current-route>svg{top:25%;height:70%}
|
|
1136
|
+
.dijiang-poster{height:auto;min-height:min(100svh,72rem);display:grid;grid-template-columns:minmax(0,42%) minmax(0,1fr);grid-template-areas:'copy scene' 'metrics scene' 'rail rail' 'footer footer';grid-template-rows:minmax(24rem,auto) auto auto auto;gap:1.25rem 3%;padding:6rem 5.1% 1rem;align-content:start}
|
|
1137
|
+
.dijiang-poster>.copy{position:relative;inset:auto;grid-area:copy;align-self:start;width:auto;max-width:100%;padding:1rem;background:#f8f9f6}.dijiang-poster>.field-copy-panel{display:none}
|
|
1138
|
+
.dijiang-poster>.metrics{position:relative;inset:auto;grid-area:metrics;width:100%;margin:0}.dijiang-poster>.rail{position:relative;inset:auto;grid-area:rail;width:auto}.dijiang-poster>.footer{position:relative;inset:auto;grid-area:footer;width:auto;margin:0}
|
|
1139
|
+
.dijiang-poster>.field-blueprint{top:7rem;bottom:12rem;left:51%;right:4%;width:auto;min-height:18rem}.dijiang-poster>.field-readout{left:65%;top:40%;width:25%;max-width:20rem}
|
|
1140
|
+
@media(max-width:56.249rem){.poster .title{font-size:2.5rem}.poster .task{font-size:1rem}.poster-layout{grid-template-rows:auto 17.5rem auto auto auto auto}.poster-layout .current-route{min-height:0}.poster-layout .route-nodes{min-height:0}.dijiang-poster{grid-template-columns:1fr;grid-template-areas:'copy' 'metrics' 'rail' 'footer';grid-template-rows:auto auto auto auto;gap:1rem;padding:6rem 1rem 1.25rem;min-height:100svh}.dijiang-poster>.field-blueprint,.dijiang-poster>.field-readout{display:none}.dijiang-poster>.copy{margin:0;width:100%}.dijiang-poster>.rail{grid-template-columns:repeat(5,minmax(0,1fr))}.dijiang-poster>.footer{font-size:.75rem;overflow-wrap:anywhere}}
|
|
1141
|
+
@media print{.loader{display:none!important}*,*:before,*:after{animation:none!important}.poster{break-after:page}}
|
|
1142
|
+
`;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export function projectFile(projectPath: string, value: string): Promise<string>;
|
|
2
|
+
export function readBoundedFile(path: string, maxBytes: number, signal?: AbortSignal): Promise<Buffer>;
|
|
3
|
+
export function parseJson(source: string | Buffer): unknown;
|
|
4
|
+
export function atomicProjectWrite(projectPath: string, value: string, content: string): Promise<void>;
|
|
5
|
+
export function withProjectLock<T>(projectPath: string, action: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { mkdir, open, realpath, rename, unlink } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
export async function projectFile(projectPath, value) {
|
|
7
|
+
const root = await realpath(resolve(projectPath));
|
|
8
|
+
const candidate = resolve(root, value);
|
|
9
|
+
const suffix = [];
|
|
10
|
+
let probe = candidate;
|
|
11
|
+
while (true) {
|
|
12
|
+
try { probe = resolve(await realpath(probe), ...suffix.reverse()); break; }
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code !== "ENOENT") throw error;
|
|
15
|
+
suffix.push(basename(probe));
|
|
16
|
+
const parent = dirname(probe);
|
|
17
|
+
if (parent === probe) throw error;
|
|
18
|
+
probe = parent;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
for (const path of [candidate, probe]) {
|
|
22
|
+
const rel = relative(root, path);
|
|
23
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error("Path must resolve inside projectPath.");
|
|
24
|
+
}
|
|
25
|
+
return candidate;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function readBoundedFile(path, maxBytes, signal) {
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
const file = await open(path, "r");
|
|
31
|
+
try {
|
|
32
|
+
const details = await file.stat();
|
|
33
|
+
if (!details.isFile() || details.size > maxBytes) throw new Error(`File exceeds the ${maxBytes} byte safety limit or is not a regular file.`);
|
|
34
|
+
// Detect growth after stat without an unbounded readFile.
|
|
35
|
+
const bytes = Buffer.alloc(Math.min(details.size + 1, maxBytes + 1));
|
|
36
|
+
let count = 0;
|
|
37
|
+
while (count < bytes.length) {
|
|
38
|
+
signal?.throwIfAborted();
|
|
39
|
+
const { bytesRead } = await file.read(bytes, count, bytes.length - count, null);
|
|
40
|
+
if (!bytesRead) break;
|
|
41
|
+
count += bytesRead;
|
|
42
|
+
}
|
|
43
|
+
if (count > details.size || count > maxBytes) throw new Error("File changed during bounded read; retry capture.");
|
|
44
|
+
return bytes.subarray(0, count);
|
|
45
|
+
} finally { await file.close(); }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseJson(source) { return JSON.parse(String(source).replace(/^\uFEFF/, "")); }
|
|
49
|
+
|
|
50
|
+
export async function atomicProjectWrite(projectPath, value, content) {
|
|
51
|
+
const path = await projectFile(projectPath, value);
|
|
52
|
+
await mkdir(dirname(path), { recursive: true });
|
|
53
|
+
const temporary = `${path}.tmp-${randomUUID()}`;
|
|
54
|
+
try {
|
|
55
|
+
const file = await open(await projectFile(projectPath, temporary), "wx", 0o600);
|
|
56
|
+
try { await file.writeFile(content, "utf8"); } finally { await file.close(); }
|
|
57
|
+
await projectFile(projectPath, value);
|
|
58
|
+
await rename(temporary, path);
|
|
59
|
+
} finally { await unlink(temporary).catch((error) => { if (error.code !== "ENOENT") throw error; }); }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function withProjectLock(projectPath, action, signal) {
|
|
63
|
+
const directory = await projectFile(projectPath, ".showcase");
|
|
64
|
+
await mkdir(directory, { recursive: true });
|
|
65
|
+
const path = await projectFile(projectPath, ".showcase/.write.lock");
|
|
66
|
+
const deadline = Date.now() + 30_000;
|
|
67
|
+
let lock;
|
|
68
|
+
while (!lock) {
|
|
69
|
+
signal?.throwIfAborted();
|
|
70
|
+
try { lock = await open(path, "wx", 0o600); }
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error.code !== "EEXIST") throw error;
|
|
73
|
+
if (Date.now() >= deadline) throw new Error("Project output is locked by another capture/generation. If its process crashed, verify it has stopped before removing .showcase/.write.lock.");
|
|
74
|
+
await delay(30, undefined, { signal });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await lock.writeFile(JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
|
|
79
|
+
signal?.throwIfAborted();
|
|
80
|
+
return await action();
|
|
81
|
+
} finally { await lock.close(); await unlink(path); }
|
|
82
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const rules = [
|
|
2
|
+
["private-key", /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED_PRIVATE_KEY]"],
|
|
3
|
+
["authorization", /\b(authorization[\"']?\s*[:=]\s*[\"']?)(?:(?:bearer|basic)\s+)?[^\s,;\"'}]+/gi, "$1[REDACTED]"],
|
|
4
|
+
["cookie", /\b((?:set-cookie|cookie)[\"']?\s*[:=]\s*)[^\r\n]+/gi, "$1[REDACTED_COOKIE]"],
|
|
5
|
+
["token", /\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|password|secret)[\"']?\s*[:=]\s*(?:\"[^\"]*\"|'[^']*'|[^\s,;}]+)/gi, "[REDACTED_TOKEN]"],
|
|
6
|
+
["provider-key", /\b(?:sk-(?:proj-|ant-|or-v1-)?[A-Za-z0-9_-]{16,}|AIza[0-9A-Za-z_-]{20,}|xai-[A-Za-z0-9_-]{16,}|pplx-[A-Za-z0-9_-]{16,}|hf_[A-Za-z0-9_-]{16,})\b/g, "[REDACTED_KEY]"],
|
|
7
|
+
["github-token", /\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g, "[REDACTED_GITHUB_TOKEN]"],
|
|
8
|
+
["url-credentials", /\b(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, "$1[REDACTED]@"],
|
|
9
|
+
["windows-path", /\b[A-Za-z]:[\\/][^\s<>:"|?*]+/g, "[REDACTED_PATH]"],
|
|
10
|
+
["unix-path", /(?<![\w/])\/(?:Users|home|mnt|var|tmp)\/[^\s/:]+(?:\/[^\s:]+)*/g, "[REDACTED_PATH]"],
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function redact(text) {
|
|
14
|
+
const replacements = {};
|
|
15
|
+
let redacted = String(text);
|
|
16
|
+
for (const [name, expression, replacement] of rules) {
|
|
17
|
+
redacted = redacted.replace(expression, (match) => {
|
|
18
|
+
replacements[name] = (replacements[name] ?? 0) + 1;
|
|
19
|
+
return match.replace(expression, replacement);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return { text: redacted, summary: {
|
|
23
|
+
originalLength: text.length, redactedLength: redacted.length, replacements,
|
|
24
|
+
totalReplacements: Object.values(replacements).reduce((sum, count) => sum + count, 0),
|
|
25
|
+
} };
|
|
26
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { redact } from "./redaction.js";
|
|
2
|
+
|
|
3
|
+
export function formatReportDuration(milliseconds, locale = "zh-CN") {
|
|
4
|
+
if (!Number.isFinite(milliseconds) || milliseconds < 0) return locale === "en" ? "Not recorded" : "未记录";
|
|
5
|
+
const seconds = Math.floor(milliseconds / 1000);
|
|
6
|
+
const minutes = Math.floor(seconds / 60);
|
|
7
|
+
const hours = Math.floor(minutes / 60);
|
|
8
|
+
return locale === "en"
|
|
9
|
+
? `${hours ? `${hours}h ` : ""}${minutes % 60}m ${seconds % 60}s`
|
|
10
|
+
: `${hours ? `${hours} 小时 ` : ""}${minutes % 60} 分 ${seconds % 60} 秒`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Pair only the same page and viewport. Never align unrelated captures by position.
|
|
14
|
+
export function findComparisonPair(shots) {
|
|
15
|
+
for (const after of shots) {
|
|
16
|
+
if (after.kind !== "after" || after.captureMode === "region") continue;
|
|
17
|
+
const before = shots.find((shot) => {
|
|
18
|
+
if (shot.kind !== "before" || shot.captureMode === "region") return false;
|
|
19
|
+
if (after.theme && shot.theme && after.theme !== shot.theme) return false;
|
|
20
|
+
const a = after.viewport, b = shot.viewport;
|
|
21
|
+
if (!a || !b || a.width !== b.width || a.height !== b.height) return false;
|
|
22
|
+
if ((after.captureMode ?? "viewport") !== (shot.captureMode ?? "viewport")) return false;
|
|
23
|
+
if (after.imageWidth && shot.imageWidth && after.imageWidth * shot.imageHeight !== shot.imageWidth * after.imageHeight) return false;
|
|
24
|
+
if (after.comparisonId || shot.comparisonId) return Boolean(after.comparisonId && after.comparisonId === shot.comparisonId);
|
|
25
|
+
return Boolean(after.url && shot.url && after.url.split("#")[0] === shot.url.split("#")[0]);
|
|
26
|
+
});
|
|
27
|
+
if (before) return { before, after };
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sanitizeReportExport(report) {
|
|
33
|
+
const fields = ["version", "generatedAt", "project", "task", "git", "tests", "screenshots", "redaction", "warnings", "demo", "evidenceReview"];
|
|
34
|
+
const sanitize = (value) => {
|
|
35
|
+
if (typeof value === "string") return redact(value).text;
|
|
36
|
+
if (Array.isArray(value)) return value.map(sanitize);
|
|
37
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [redact(key).text, sanitize(item)]));
|
|
38
|
+
return value;
|
|
39
|
+
};
|
|
40
|
+
return Object.fromEntries(fields.filter((key) => report?.[key] !== undefined).map((key) => [key, sanitize(report[key])]));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function reportMarkdown(report, locale = "zh-CN") {
|
|
44
|
+
const en = locale === "en";
|
|
45
|
+
const md = (value) => String(value ?? "").replace(/[\\`*_[\]<>]/g, "\\$&").replace(/[\r\n]+/g, " ");
|
|
46
|
+
const files = report.git?.files ?? [];
|
|
47
|
+
const tests = report.tests ?? [];
|
|
48
|
+
return [
|
|
49
|
+
`# ${md(report.project?.name || (en ? "Delivery report" : "交付报告"))}`,
|
|
50
|
+
"", ...(report.demo ? [en ? "> Demo data, not a verified project delivery." : "> 演示数据,不代表当前项目实测交付结果。", ""] : []),
|
|
51
|
+
md(report.task?.goal), "",
|
|
52
|
+
`- ${en ? "Status" : "状态"}: ${md(report.task?.status)}`,
|
|
53
|
+
`- ${en ? "Duration" : "耗时"}: ${formatReportDuration(report.task?.durationMs, locale)}`,
|
|
54
|
+
`- ${en ? "Revision" : "比较范围"}: ${md(report.git?.baseRef)}..${md(report.git?.headRef)}`,
|
|
55
|
+
"", `## ${en ? "Changes" : "变更清单"}`,
|
|
56
|
+
...files.map((file) => `- ${md(file.path)}: +${Number(file.additions) || 0} / -${Number(file.deletions) || 0}`),
|
|
57
|
+
"", `## ${en ? "Verification" : "验证回执"}`,
|
|
58
|
+
...tests.map((test) => `- ${md(test.command)}: ${md(test.status)} (exit ${Number(test.exitCode) || 0})`),
|
|
59
|
+
"", `## ${en ? "Privacy review" : "隐私审查"}`,
|
|
60
|
+
...Object.entries(report.redaction?.replacements ?? {}).map(([name, count]) => `- ${md(name)}: ${Number(count) || 0}`),
|
|
61
|
+
"", `## ${en ? "Review queue" : "复核事项"}`, ...(report.warnings ?? []).map((warning) => `- ${md(warning)}`), "",
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function serializeInlineJson(value) {
|
|
66
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
67
|
+
}
|
package/plugin/report-schema.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
|
|
3
3
|
const isoDate = z.string().datetime();
|
|
4
4
|
|
|
5
|
-
const taskSchema = z.object({
|
|
5
|
+
export const taskSchema = z.object({
|
|
6
6
|
id: z.string().min(1),
|
|
7
7
|
goal: z.string().min(1),
|
|
8
8
|
status: z.enum(["completed", "partial", "failed"]),
|
|
@@ -11,15 +11,21 @@ const taskSchema = z.object({
|
|
|
11
11
|
durationMs: z.number().int().nonnegative(),
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
const gitChangeSchema = z.object({
|
|
14
|
+
export const gitChangeSchema = z.object({
|
|
15
15
|
baseRef: z.string().min(1).optional(),
|
|
16
16
|
headRef: z.string().min(1).optional(),
|
|
17
|
+
baseCommit: z.string().optional(),
|
|
18
|
+
headCommit: z.string().optional(),
|
|
19
|
+
fingerprint: z.string().optional(),
|
|
17
20
|
files: z.array(z.object({
|
|
18
21
|
path: z.string().min(1),
|
|
19
22
|
status: z.enum(["added", "modified", "deleted", "renamed", "untracked"]),
|
|
20
23
|
additions: z.number().int().nonnegative(),
|
|
21
24
|
deletions: z.number().int().nonnegative(),
|
|
22
25
|
previousPath: z.string().min(1).optional(),
|
|
26
|
+
diff: z.string().max(65_536).optional(),
|
|
27
|
+
diffTruncated: z.boolean().optional(),
|
|
28
|
+
diffUnavailable: z.enum(["untracked", "binary", "oversize", "limit", "unavailable"]).optional(),
|
|
23
29
|
})),
|
|
24
30
|
summary: z.object({
|
|
25
31
|
changedFiles: z.number().int().nonnegative(),
|
|
@@ -28,7 +34,7 @@ const gitChangeSchema = z.object({
|
|
|
28
34
|
}),
|
|
29
35
|
});
|
|
30
36
|
|
|
31
|
-
const testReceiptSchema = z.object({
|
|
37
|
+
export const testReceiptSchema = z.object({
|
|
32
38
|
id: z.string().min(1).optional().default("test"),
|
|
33
39
|
command: z.string().min(1),
|
|
34
40
|
startedAt: isoDate.optional().default("1970-01-01T00:00:00.000Z"),
|
|
@@ -36,9 +42,10 @@ const testReceiptSchema = z.object({
|
|
|
36
42
|
exitCode: z.number().int(),
|
|
37
43
|
status: z.enum(["passed", "failed", "skipped"]),
|
|
38
44
|
output: z.string().optional().default(""),
|
|
45
|
+
outputTruncated: z.boolean().optional(),
|
|
39
46
|
});
|
|
40
47
|
|
|
41
|
-
const screenshotEvidenceSchema = z.object({
|
|
48
|
+
export const screenshotEvidenceSchema = z.object({
|
|
42
49
|
id: z.string().min(1).optional().default("capture"),
|
|
43
50
|
label: z.string().min(1).optional().default("capture"),
|
|
44
51
|
// Accept the two legacy URL/theme aliases so reports created before the
|
|
@@ -53,18 +60,20 @@ const screenshotEvidenceSchema = z.object({
|
|
|
53
60
|
imagePath: z.string().min(1),
|
|
54
61
|
capturedAt: isoDate.optional().default("1970-01-01T00:00:00.000Z"),
|
|
55
62
|
kind: z.enum(["after", "before"]).optional().default("after"),
|
|
63
|
+
deviceScaleFactor: z.number().positive().max(8).optional(),
|
|
64
|
+
captureMode: z.enum(["viewport", "full-page", "region"]).optional(),
|
|
65
|
+
sourceFingerprint: z.string().optional(),
|
|
66
|
+
comparisonId: z.string().min(1).max(128).optional(),
|
|
56
67
|
});
|
|
57
68
|
|
|
58
|
-
const redactionSummarySchema = z.object({
|
|
69
|
+
export const redactionSummarySchema = z.object({
|
|
59
70
|
originalLength: z.number().int().nonnegative().optional().default(0),
|
|
60
71
|
redactedLength: z.number().int().nonnegative().optional().default(0),
|
|
61
72
|
replacements: z.record(z.string(), z.number().int().nonnegative()).default({}),
|
|
62
73
|
totalReplacements: z.number().int().nonnegative().default(0),
|
|
63
74
|
});
|
|
64
75
|
|
|
65
|
-
//
|
|
66
|
-
// the plugin because the published plugin is plain ESM JavaScript and cannot
|
|
67
|
-
// import the source-only TypeScript module at runtime.
|
|
76
|
+
// One runtime schema for the published plugin, source CLI and browser fixtures.
|
|
68
77
|
export const reportSchema = z.object({
|
|
69
78
|
version: z.literal(1),
|
|
70
79
|
generatedAt: isoDate,
|
|
@@ -74,6 +83,7 @@ export const reportSchema = z.object({
|
|
|
74
83
|
tests: z.array(testReceiptSchema),
|
|
75
84
|
screenshots: z.array(screenshotEvidenceSchema),
|
|
76
85
|
redaction: redactionSummarySchema,
|
|
86
|
+
warnings: z.array(z.string()).optional(),
|
|
77
87
|
}).passthrough();
|
|
78
88
|
|
|
79
89
|
export function formatReportIssues(error) {
|