Files
XingliGe 390fcf88f3 fix(plugin-previews): keep Community gallery previews in sync with shipped plugins (#5262)
* fix(plugin-previews): validate baked preview metadata against shipped plugins

The Home Community gallery renders whatever data/plugin-previews/manifest.json
names, and nothing kept that metadata in sync with reality: #4815 deleted 19
example plugins but left all 19 manifest entries behind, #4040 shipped a card
showing another plugin's imagery, a failed re-bake silently leaves the old
clip serving outdated content, and durationMs recorded the intended walk time
rather than the encoded file (23 entries carried holdMs > real duration, so
the card's idle loop pointed past the end of the clip).

- add scripts/check-plugin-preview-manifest.ts to pnpm guard: every manifest
  entry must name a shipped plugin, own its clip keys (<id>/<hash>/... or the
  legacy <id>.<hash>....), carry a coherent fingerprint, and never share a
  clip with another entry; prune the 19 orphaned entries it caught
- bake validation (BAKE_VERSION 5): reject blank clips (whole-clip luma range
  via ffprobe signalstats), record durationMs from the encoded file, clamp
  holdMs to it, and write a bake-report.json naming skipped/stale/blank
  plugins instead of swallowing the failure
- strict mode (--strict / PREVIEW_STRICT=1) fails the run when a bake left
  broken metadata behind; the pre-merge validation workflow now runs strict
  while the post-merge/nightly publishers stay lenient

* fix(plugins): make the hyperframes example self-contained

The example loaded cdn.tailwindcss.com (a render-blocking head script the
page never used — every style is inline) plus Google Fonts. Inside the
sandboxed gallery iframe or on a slow network the stalled script showed a
long blank instead of the composition, and the CI bake raced the webfonts.
Drop both: the page now renders from local system font stacks with zero
external requests, so the live preview, the detail iframe, and the baked
clip all show the same content.

* fix(plugin-previews): address review — fail fast on probe errors, widen strict trigger paths

- probeClipMs/clipLumaRange now throw instead of returning null: ffprobe is
  required validation infrastructure, and a swallowed probe error silently
  disabled exactly the checks this PR adds. A thrown error becomes an
  'error …' skip, is recorded in bake-report.json under 'errors', and fails
  strict mode — while the routine skips every sweep has (non-html plugins
  404ing the preview route) still never trip strict.
- bake-plugin-previews-pr.yml now also triggers on plugins/community/**,
  .github/actions/bake-previews/**, and itself, so community-plugin preview
  changes and bake-recipe changes cannot merge without the strict pre-merge
  validation.

* fix(plugin-previews): bake static pages as a held still instead of skipping forever

The strict pre-merge bake on this PR caught five manifest entries that could
NEVER refresh: example-dating-web, example-html-ppt-zhangzara-8-bit-orbit,
example-frame-logo-outro, od-new-generation, example-orbit-notion. All five
are (near-)static pages — the CDP screencast only delivers frames when
something repaints, so they produce 1-4 frames and the old 'frames < 5' guard
skipped them on every bake, nightly included, leaving their committed entries
permanently stale (their old clips are the 103-210ms degenerate files in the
manifest today).

Encode <5-frame captures as a still instead: hold the last frame for the
idle-loop span (half before the trailing repeated concat entry, half
inherited by it, summing to ~HOLD_MS). Only a zero-frame capture is a real
failure now. The blank-luma check still rejects stills of nothing.

* fix(plugin-previews): never persist a manifest entry without a fingerprint

When the preview fingerprint fetch failed, the loop still rendered and — on
an otherwise successful bake — persisted an entry with hash: null and
un-fingerprinted keys: metadata the content-hash reuse skip can never match
again and the new manifest guard rejects. Treat a missing fingerprint as an
infrastructure error instead: skip the plugin, record it under errors in
bake-report.json (strict mode exits non-zero), and leave any committed entry
untouched for the next sweep.

---------

Co-authored-by: Tuola Ge <gexingli@refly.ai>
2026-07-08 03:44:05 +00:00

148 lines
7.2 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Hyperframes · AI workflow 三件套</title>
<!-- Self-contained on purpose: no CDN scripts or webfont fetches. This page is
rendered inside the sandboxed gallery iframe and by the CI preview bake,
where an external dependency that stalls or is blocked shows as a blank
card instead of the composition. -->
<style>
:root {
--font-display: 'Inter Tight', -apple-system, 'SF Pro Display', 'Segoe UI',
'PingFang SC', 'Hiragino Sans GB', 'Noto Sans SC', 'Noto Sans CJK SC',
'Microsoft YaHei', sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
}
body { margin:0; background:#000; font-family:var(--font-display); color:#fff; -webkit-font-smoothing:antialiased; }
.frame { position:fixed; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; padding:96px; opacity:0; transition:opacity 0.6s ease; pointer-events:none; }
.frame.active { opacity:1; pointer-events:auto; }
.grid-bg::before { content:""; position:absolute; inset:0; opacity:0.08; background-image:linear-gradient(#fff 1px,transparent 1px),linear-gradient(90deg,#fff 1px,transparent 1px); background-size:64px 64px; }
.glow { position:absolute; width:1000px; height:1000px; border-radius:50%; filter:blur(180px); pointer-events:none; }
.num { position:absolute; top:48px; left:48px; font-family:var(--font-mono); font-size:14px; letter-spacing:0.18em; opacity:0.55; }
.controls { position:fixed; bottom:32px; left:50%; transform:translateX(-50%); display:flex; align-items:center; gap:14px; padding:10px 18px; border-radius:999px; background:rgba(255,255,255,0.06); backdrop-filter:blur(12px); border:1px solid rgba(255,255,255,0.12); font-family:var(--font-mono); font-size:12px; z-index:50; }
.controls button { background:none; border:none; color:#fff; cursor:pointer; opacity:0.7; }
.controls button:hover { opacity:1; }
.progress { position:fixed; top:0; left:0; height:3px; background:#e9b94a; transition:width 0.2s linear; z-index:50; }
h1 { font-family:var(--font-display); font-weight:900; line-height:0.95; letter-spacing:-0.04em; margin:0; text-align:center; }
.hook h1 { font-size:160px; }
.em { font-family:Georgia,serif; font-style:italic; font-weight:700; color:#e9b94a; }
.label { font-size:14px; font-weight:600; letter-spacing:0.28em; text-transform:uppercase; opacity:0.55; margin-bottom:32px; }
</style>
</head>
<body>
<div class="progress" id="progress" style="width:0%"></div>
<section class="frame active hook" data-duration="3000">
<div class="num">FRAME 01 / 08 · HOOK</div>
<div class="glow" style="background:#c96442; top:-200px; left:-200px"></div>
<div class="label">观察</div>
<h1>80% 的 AI 工具用户,<br/><span class="em">没用对</span> prompt。</h1>
</section>
<section class="frame" data-duration="3000">
<div class="num">FRAME 02 / 08</div>
<h1 style="font-size:128px">不是因为<br/><span class="em">prompt 难写</span></h1>
</section>
<section class="frame" data-duration="3000">
<div class="num">FRAME 03 / 08</div>
<h1 style="font-size:128px">是因为他们<br/>一直在 <span class="em">重写</span></h1>
</section>
<section class="frame" data-duration="4000">
<div class="num">FRAME 04 / 08</div>
<div class="label" style="color:#e9b94a"></div>
<h1 style="font-size:140px">真正高效的人,<br/><span class="em">模板</span></h1>
</section>
<section class="frame grid-bg" data-duration="4000">
<div class="num">FRAME 05 / 08</div>
<div class="label">公式</div>
<h1 style="font-size:88px; max-width:24ch">
模板 = <span style="color:#c96442">提示词</span><br/>
+ <span style="color:#e9b94a">设计 system</span><br/>
+ <span style="color:#6c3aa6">输出格式</span>
</h1>
</section>
<section class="frame" data-duration="5000">
<div class="num">FRAME 06 / 08</div>
<div class="glow" style="background:#e9b94a; bottom:-300px; right:-200px"></div>
<h1 style="font-size:120px">HTML <span class="em">Anything</span><br/>把这三件事<br/>打包好了。</h1>
</section>
<section class="frame" data-duration="5000">
<div class="num">FRAME 07 / 08</div>
<div class="label" style="color:#e9b94a">行动</div>
<h1 style="font-size:160px"><span class="em">9 套模板,</span><br/>一个动作:<br/><span style="font-family:var(--font-mono); font-style:normal; color:#fff">⌘+Enter</span></h1>
</section>
<section class="frame" data-duration="3000">
<div class="num">FRAME 08 / 08 · CTA</div>
<div class="glow" style="background:#c96442; top:50%; left:50%; transform:translate(-50%,-50%); opacity:0.6"></div>
<h1 style="font-size:96px">现在试试 →</h1>
<div style="font-family:var(--font-mono); font-size:24px; margin-top:36px; opacity:0.85">github.com/your-org/html-anything</div>
</section>
<div class="controls">
<button id="prev" title="上一帧"></button>
<button id="play" title="播放 / 暂停"></button>
<button id="next" title="下一帧"></button>
<span id="counter" style="opacity:0.55">01 / 08</span>
</div>
<!-- HYPERFRAMES_META: {"frames":[
{"i":1,"duration":3000,"transition":"fade","scene":"Hook: 80% 的人没用对 prompt"},
{"i":2,"duration":3000,"transition":"fade","scene":"不是因为难写"},
{"i":3,"duration":3000,"transition":"fade","scene":"是因为重写"},
{"i":4,"duration":4000,"transition":"fade","scene":"真正高效的用模板"},
{"i":5,"duration":4000,"transition":"fade","scene":"模板 = 提示词 + 设计 + 格式"},
{"i":6,"duration":5000,"transition":"fade","scene":"HTML Anything 打包"},
{"i":7,"duration":5000,"transition":"fade","scene":"9 套模板, ⌘+Enter"},
{"i":8,"duration":3000,"transition":"fade","scene":"CTA: github.com/your-org/html-anything"}
]} -->
<script>
const frames = Array.from(document.querySelectorAll('.frame'));
const total = frames.length;
const counter = document.getElementById('counter');
const playBtn = document.getElementById('play');
const progress = document.getElementById('progress');
let i = 0;
let playing = true;
let frameStart = Date.now();
let timer;
function show(n) {
i = (n + total) % total;
frames.forEach((f,k)=>f.classList.toggle('active', k===i));
counter.textContent = String(i+1).padStart(2,'0') + ' / ' + String(total).padStart(2,'0');
frameStart = Date.now();
if (playing) schedule();
}
function schedule() {
clearTimeout(timer);
const d = Number(frames[i].dataset.duration) || 3000;
timer = setTimeout(()=> show(i+1), d);
}
function tick() {
const d = Number(frames[i].dataset.duration) || 3000;
const pct = Math.min(1, (Date.now()-frameStart)/d) * 100;
progress.style.width = pct + '%';
requestAnimationFrame(tick);
}
document.getElementById('prev').onclick = ()=>show(i-1);
document.getElementById('next').onclick = ()=>show(i+1);
playBtn.onclick = ()=> { playing=!playing; playBtn.textContent = playing?'⏸':'▶'; if(playing){schedule(); frameStart=Date.now();} else clearTimeout(timer); };
document.addEventListener('keydown', e => {
if (e.key==='ArrowRight') show(i+1);
else if (e.key==='ArrowLeft') show(i-1);
else if (e.key===' ') { e.preventDefault(); playBtn.click(); }
});
show(0);
tick();
</script>
</body>
</html>