一鍵還原任意網站,這套 Playwright + 多 Agent 工程流太猛了

一 前言

哈囉,大家好,我是 alien,今天我們來講一個能夠將精美網站一鍵複製的功能——ai-website-cloner-template 的使用和實作原理。

透過本篇文件你能收穫到哪些內容?

  • AI 自動逆向解析網頁的新思路;
  • Playwright 使用介紹;
  • ai-website-cloner-template 的介紹;

ai-website-cloner-template 的用法十分簡單:

第一步:把 GitHub 上的模板下載到本地:

js 代碼解讀複製代碼git clone https://github.com/JCodesMore/ai-website-cloner-template.git

第二步:執行 Claude Code,執行 /clone-website(這個本質上是模板工程維度的 skill)

3dfdca83a7eb81576305b5fc2833fe5d.png

廢話不說,用掘金的網站為例子(juejin.cn),我們先來看一下實現的效果:

pc 端效果對比(左:生成/右:原圖):

bcf4f44235c451291204bf95bb67a957.png

移動端效果對比(左:生成/右:原圖):

cd1b1a44d37879542b551e1cec909794.png

多端相容場景:

2026-07-048.14.44-ezgif.com-video-to-gif-converter.gif

生成的程式碼整體效果評估下來:

  • 整體 UI 還原還行,沒有達到預期那麼完美,還原度親測能到 70% 以上;
  • 對於一些細節,移動端適配做得比較好;
  • 生成的程式碼品質、可維護性還可以;(這個本質上受到模型本身影響比較大);

接下來我們來看一下 ai-website-cloner-template 內部是如何處理的。

二 核心實作

2.1 核心原理

138c7c494a76e62e4047bcf0308ab52a.png

ai-website-cloner-template 原理可以分為四個部分:

Phase 1:摸底(Reconnaissance)

拿到目標 URL 後,第一件事是搞清楚「它到底長啥樣、怎麼動」。

  • 截圖:用 Playwright 啟一個真瀏覽器,在三個標準視口(1440×900、768×1024、390×844)下分別截全頁圖和首屏圖。為什麼要三個視口?因為響應式斷點不是猜的,是要驗證的。
  • 抓 design tokens:在瀏覽器裡跑 JS,遍歷頁面上所有 DOM 節點,呼叫 getComputedStyle() 把每個節點的字型、字號、顏色、背景、圓角、間距全部聚合去重。這不是抄 CSS 原始碼,而是讀最終生效的計算值——能穿透 @apply、CSS 變數、媒體查詢。
  • 互動掃描:靜態截圖只能看到「現在長啥樣」,但 tab 切換會換內容、捲動可能改 header 樣式、hover 可能讓卡片變色。所以還要模擬捲動到不同位置、點擊 tab、滑鼠懸停,把每個互動態都抓一份。
  • 產出:一堆截圖 + 一堆 JSON(tokens.json、assets.json、topology.json 等),全在 docs/research/ 和 docs/design-references/ 下。

我們來看一下:

05b16ab2b73f8287c4b3a9cdf0498ce8.png

c2d06a90278674467aabd50efeaf39a3.png

215ca30db1f8b3a7d8a605ac1caf930b.png

Phase 2:搭骨架(Foundation)

摸完底,開始鋪地基。這一步必須先做完,後續所有元件才能開工。

  • design tokens 落地:把 Phase 1 抓出來的顏色、字號映射到 Tailwind v4 的 @theme 區塊和 :root CSS 變數。這樣後面寫元件時,bg-primary、text-muted-foreground 這些 class 直接可用,不用每次查色值。
  • 字型策略:根據目標站的字型族決定怎麼載入。如果目標站用 Google Fonts,直接走 next/font/google;如果像掘金這種純系統字型(PingFang SC、Microsoft YaHei),就退回到 system font stack,省掉外網請求。
  • 圖示去重:目標站頁面裡可能有三四十個 SVG 節點,但很多是重複的。用腳本去重後只剩十幾個唯一的,把它們整理成 React 元件放到 icons.tsx。
  • 資源下載:把頁面裡的所有圖片、logo、favicon 用腳本批次下載到 public/images/ 和 public/seo/,按用途分目錄。下載不了的佔位圖,用基於 id 的 hash 漸層色塊頂替(每個 id 顏色唯一,視覺上合理)。
  • 產出:globals.css 配好、icons.tsx 配好、public/images/ 配好。

Phase 3:元件生成(Spec & Build)

這是流水線最關鍵的階段,也是最能體現「規約驅動」思想的地方。

  • 寫 spec 檔案:先不為每個 section 寫程式碼,而是先寫一份 markdown 格式的規約文件(*.spec.md)。這份文件是 builder agent 的施工合約,裡面寫清楚:

    1. 目標元件長啥樣(截圖路徑)
    2. 像素級別的樣式(字號 17px、padding 12px 20px 0px、顏色 #252933,全是從 getComputedStyle 拿的真實值,不是「差不多」)
    3. 真實的內容資料(標題、作者、縮圖,全是從目標 DOM 抓的真實文字,不是 Lorem Ipsum)
    4. 互動模型(hover 會怎樣、點擊會怎樣、有沒有捲動驅動)
  • 複雜度預算:單份 spec 不能超過 150 行。掘金首頁 12 個 section 就拆 12 份 spec,分別派給 12 個 builder agent 幹。拆得越細,單個 agent 任務越聚焦,品質越高。
  • worktree 並發:每個 builder agent 在獨立的 git worktree 分支上工作,互不衝突。做完後由 orchestrator 逐個 merge 進 main,每次 merge 立刻跑一次 npm run check(lint + typecheck + build)。這就是「Build Must Always Compile」原則——main 分支永遠保持可建置狀態。
  • 產出:每個 section 一個 React 元件檔案 + 它對應的 spec 檔案。

121af4ffe5e688769d4854a5fd094b78.png

Phase 4:拼裝與 QA(Assembly & QA)

所有元件都寫完了,現在要拼成完整頁面,並驗證它跟目標站是否真的像。

  • 拼裝:page.tsx 按 z-index 和斷點把所有元件組合起來。比如 header 是 sticky top(z-index: 250),行動端的浮動下載按鈕要 position: fixed(z-index: 100),不能擋住 header 但要能蓋住 footer。
  • 視覺 QA:用腳本同時截三個視口下的「目標站」和「我們做的」,生成 target-.png 和 clone-.png,並排對比。優先級:

    1. 尺寸差異(最容易暴露——寬高、padding、gap)
    2. 顏色差異(肉眼最敏感——背景、文字、邊框)
    3. 版面差異(flex 方向、grid 欄數、sticky 是否失效)

在實作過程中,核心的邏輯就是要真實地取得瀏覽器裡面的頁面狀態,Playwright 起到了核心作用。

2.2 Playwright 介紹

先看一下 Playwright 是什麼?

Playwright 是一個由 Microsoft 開源的、用於端到端測試和瀏覽器自動化的函式庫。它讓你用程式碼控制真實的瀏覽器(Chrome、Firefox、Safari),自動完成點擊、填寫、截圖、讀取頁面內容等操作。

比如我們想在 Node 環境喚起瀏覽器,那麼可以直接呼叫對應的 API:

js 代碼解讀複製代碼// 1. 啟動瀏覽器
const { chromium } = require('playwright');
const browser = await chromium.launch({ headless: true });  

還可以建立一個獨立的隱身視窗:

js 代碼解讀複製代碼// 2. 新建一個 context(獨立隱身視窗)                                                                                                                 
const context = await browser.newContext(); 

操作瀏覽器,本質上我們可以借助這個能力去完成 e2e 等自動化測試驗證:

js 代碼解讀複製代碼// 點擊
await page.click('button#submit');  
// 導航跳轉
await page.goto('https://example.com'); 

還可以截圖用來取得效果:

js 代碼解讀複製代碼await page.screenshot({ path: 'screenshot.png' });                                                                                                

感興趣的同學可以研究一下,這裡是整理的一些 API 能力:

1bbe988dbfe40bc21918c37a106a3d51.png

三 具體實作思路

介紹完核心原理,我們挑一些重點場景,來看一下核心實作:

多視口截圖:

typescript 代碼解讀複製代碼const viewports = [
  { name: "desktop-1440", width: 1440, height: 900 },
  { name: "tablet-768", width: 768, height: 1024 },
  { name: "mobile-390", width: 390, height: 844 },
];

for (const vp of viewports) {
  const ctx = await browser.newContext({
    viewport: { width: vp.width, height: vp.height },
    locale: "zh-CN",
    userAgent: "Mozilla/5.0 ... Chrome/131.0.0.0 ...",
  });
  const page = await ctx.newPage();
  await page.goto(url, { waitUntil: "networkidle", timeout: 60000 });
  await page.waitForTimeout(2500); // 等 hydration
  await page.screenshot({ path: `${outDir}/${vp.name}-full.png`, fullPage: true });
  await ctx.close();
}

跨三個標準視口各截一張 fullPage 圖,注意 waitUntil: "networkidle" 後還要再等 2.5 秒 —— juejin 用了大量懶載入圖片和 IntersectionObserver,networkidle 不等於「完全穩定」。

樣式還原:提取全域樣式 tokens:

page.evaluate() 跑一段 JS,遍歷所有 DOM 節點呼叫 getComputedStyle()

typescript 代碼解讀複製代碼const tokens = await page.evaluate(() => {
  const all = document.querySelectorAll("*");
  const fonts = new Set(), sizes = new Set(), colors = new Set();
  for (const el of all) {
    const s = getComputedStyle(el);
    fonts.add(s.fontFamily);
    sizes.add(s.fontSize);
    if (s.color !== "rgba(0, 0, 0, 0)") colors.add(s.color);
  }
  return { fonts: [...fonts], sizes: [...sizes].sort(), colors: [...colors] };
});

juejin 的真實 token 抓出來後是這個樣子:

text 代碼解讀複製代碼主色藍:  rgb(30, 128, 255)  →  #1e80ff
頁面底:  rgb(242, 243, 245) →  #f2f3f5
卡片底:  rgb(255, 255, 255) →  #ffffff
正文:    rgb(37, 41, 51)    →  #252933
次要文字:  rgb(81, 87, 103)   →  #515767
三級文字:  rgb(134, 144, 156) →  #86909c
邊框:    rgb(228, 230, 235) →  #e4e6eb
危險紅:  rgb(246, 66, 66)   →  #f64242
字型族:  PingFang SC, Microsoft YaHei, Helvetica Neue

強制性交互掃描:

這一步是整個 skill 的精華。靜態截圖只能看見「現在長什麼樣」,但站點是「活」的:tab 切換會換內容、捲動會讓 header 變樣式、hover 會讓卡片換底色。

以 juejin 為例做一遍完整 sweep:

typescript 代碼解讀複製代碼// 捲動 sweep:截不同 scrollY 下的 header 樣式
for (const y of [0, 300, 600, 900, 1200, 1500, 1800, 2094]) {
  await page.evaluate((sy) => window.scrollTo({ top: sy, behavior: "instant" }), y);
  await page.waitForTimeout(400);
  const data = await page.evaluate(() => {
    const header = document.querySelector("header");
    return {
      scrollY: window.scrollY,
      headerBg: header ? getComputedStyle(header).backgroundColor : null,
      headerShadow: header ? getComputedStyle(header).boxShadow : null,
    };
  });
  scrollSnapshots.push(data);
}

// 點擊 sweep:點「最新」 tab 看內容是否變化
const tabClick = await page.evaluate(() => {
  const target = [...document.querySelectorAll("a, button, div, span")].find(
    (el) => el.textContent?.trim() === "最新" && el.children.length === 0
  );
  if (!target) return { ok: false };
  target.click();
  return { ok: true };
});
await page.waitForTimeout(1500);
const latestContent = await page.evaluate(() => {
  return document.querySelectorAll(".entry").length; // 真實資料:1 張卡
});

實測發現 juejin 的「最新」 tab 切換後文章數從 16 降到 1,說明這不是純 UI 切換,是真實重新拉資料。克隆時用 useState 切兩份靜態 mock,但生產部署需要後端 API 配合。

頁面拓撲對映:

把所有 section 列出來,標註互動模型:

text 代碼解讀複製代碼1. Header       — 靜態 (無 scroll 變化)
2. LeftSidebar  — 點擊 (導覽)
3. ArticleList  — 點擊 (tab 切換)
4. ArticleCard  — hover (subtle bg)
5. CtaBanner    — 靜態
6. RightRail    — 靜態 (重新整理按鈕 mock)
7. Footer       — 靜態
8. MobileAppPill — 靜態 (position: fixed)

這一步的產出是 PAGE_TOPOLOGY.md,作為後續 spec 檔案的索引。

d2413f24dfb89d57ada3b56c89939dbe.png

如上就是 Phase 1 部分,後面兩個部分會對樣式、資源,以及生成元件的規範 spec 進行系統化處理,這裡就不過多介紹了。我們看一下 Phase 4 部分是如何做視覺 Diff 的。

QA 腳本遍歷三個視口分別截 target 和 clone:

typescript 代碼解讀複製代碼for (const vp of viewports) {
  const ourCtx = await browser.newContext({ viewport: vp });
  const ourPage = await ourCtx.newPage();
  await ourPage.goto("http://localhost:3010/", { waitUntil: "networkidle" });
  await ourPage.screenshot({ path: `clone-${vp.name}-full.png`, fullPage: true });

  const targetCtx = await browser.newContext({ viewport: vp });
  const targetPage = await targetCtx.newPage();
  await targetPage.goto("https://juejin.cn/", { waitUntil: "networkidle" });
  await targetPage.screenshot({ path: `target-${vp.name}-full.png`, fullPage: true });
}

對比找差異的優先級:

  1. 尺寸差異:寬高、padding、gap(最容易暴露)
  2. 顏色差異:背景、文字、邊框(肉眼最敏感)
  3. 版面差異:flex 方向、grid 欄數、sticky 是否失效

juejin 實戰中 mobile 一次性暴露了 3 個差異(tag wrap、author truncate、button-in-anchor),desktop/tablet 一次通過。

四 總結

ai-website-cloner-template 的真正價值,不在於「能複製網站」——這本身就是 DevTools 的本職工作。它把一次看似簡單的「截圖 + 抄程式碼」,重構成了一條可重用、可驗證、可並發的工程流水線:先 Reconnaissance 抓計算值、再 Foundation 搭骨架、再用 Spec & Build 的規約驅動多 Agent 並行生成、最後 Assembly & QA 做多視口視覺回歸。先寫 spec、再寫程式碼這個順序的顛倒,是從「AI 玩具」邁向「AI 工程」的分水嶺。

建議大家 clone 一份到本地,挑一個欣賞的站點跑一遍,親身感受「規約驅動 + 多 Agent 並行」的真實體感。先動手的人,先看到未來。感謝閱讀,我們下篇見。希望的朋友可以按讚 + 收藏,支持更新 AI 場景的「奇思妙想」。


原文出處:https://juejin.cn/post/7661439504077176847


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝17   💬3   ❤️5
740
🥈
我愛JS
📝3   💬6   ❤️4
168
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登