Figma 建置
這裡的一切都先無聲地失敗。沒有任何東西會拋出錯誤、沒有任何東西會發出警告,而畫布看起來就是不對勁。
迴圈,優先於一切
標記建置並清除你自己的輸出。 否則,當 Figma 正在提供一個快取過的套件時,你可能會花很長時間除錯「看起來一樣」的問題。
const BUILD = "15:03"; // 每次套件更新時遞增
function clearPrevious() {
const mine = figma.currentPage.children.filter(n => /^\d\d · /.test(n.name));
mine.forEach(n => n.remove());
return mine.length;
}
figma.closePlugin("Build " + BUILD + " — replaced " + removed + " frames.");
通知訊息中有兩個數字,而且兩者都是診斷用的。過時的戳記表示套件被快取了:移除外掛程式、重新匯入 manifest、重新開啟分頁。當畫布上明顯有框架時,卻顯示 Replaced 0,也表示同樣的問題。
沒有這個機制,一個會重新建立框架的外掛程式會在現有框架上疊加第二組,而且沒有任何東西告訴你正在查看哪一組。計算框架名稱的副本數量,就是你證明程式是否真的執行過的方法。
清除並重建僅適用於首次建置。 一旦檔案進入審查階段,評論就會綁定到節點 ID。移除一個框架並繪製一個新的,會使該框架上的所有評論成為孤兒。在審查回合期間,請就地編輯現有節點(請參閱「審查迴圈」)。
一個無效值會毀掉整個建置
這個檔案中的每個失敗都有相同的模式:一個被 API 拒絕的單一數值,在繪製中途被拋出,留下一個半渲染的頁面,看起來像是「版面」問題。典型的罪魁禍首有:負數寬度、固定的 10px 文字高度、未載入的字重。
在邊界處進行箝制,而不是信任算術運算。
function W1(v, min){ const n = Number(v); return (isFinite(n) && n > 0) ? n : (min || 1); }
將每個寬度、高度和文字寬度都經由此函數處理。一次箝制只會代價一個看起來不對的方塊;一次拋出則會毀掉整個頁面。
會造成問題的算術運算是一種固定的減法,它只在一個寬度下成立:
const FEED = DW - PAD*2 - 400; // 在 1440 時是 840。在 390 時是負 50。
任何從畫布寬度減去的常數都需要一個中斷點防護,否則在較小的畫布上會變成負數。
要能辨識的症狀: 寬框架看起來完成了,而窄框架則缺失或只繪製了一半,與其他東西重疊。這不是定位錯誤。請閱讀錯誤通知訊息——它會指出屬性和節點名稱。
永遠不要在原點繪製
一個從 (0, 0) 開始的外掛程式會落在已經存在的任何東西上面,而一個只清除自己框架的清理程式也無法拯救你:兩個共用一個頁面的外掛程式都會認為畫布是空的。
先測量,再開始清除:
let startY = 0;
figma.currentPage.children.forEach(n => { startY = Math.max(startY, n.y + n.height); });
if (figma.currentPage.children.length) startY += 600;
在畫布上為每一行加上標籤,並讓清理程式匹配建置產出的所有內容——框架和標籤——否則重新執行會留下孤立的文字。
文字大小設定,依照這個確切順序
這是代價最高的錯誤。Figma 在文字的內容改變時重新計算其高度,而不是在你調整它大小的時候。
const str = String(s);
const t = figma.createText();
t.fontName = { family: "Inter", style: weight };
t.characters = str; // 1. 首先
t.fontSize = size; // 2. 接著樣式
t.lineHeight = { unit: "PERCENT", value: 145 };
if (width) {
t.textAutoResize = "HEIGHT"; // 3. 在空節點上設定會**無效**
t.resize(width, t.height); // 永遠不要使用文字字面高度
t.characters = str; // 4. 強制在新寬度下重新測量
} else {
t.textAutoResize = "WIDTH_AND_HEIGHT";
}
- 在節點有字符之前設定
textAutoResize = "HEIGHT"會被忽略。 將內容移到最後卻不移動該屬性,會使情況更糟。 resize(w, 10)會將高度永久固定在 10。請傳入t.height。- 重新賦予相同的字串是雙重保險的重新測量方法。
症狀: 所有具有明確寬度的文字約為 10px 高,所有沒有明確寬度的文字則是正確的,而所有高度僅來自文字框架都會崩潰到其內邊距。
width 意味著固定寬度——哪個軸並不明顯
if (opts.horizontal) {
f.primaryAxisSizingMode = width ? "FIXED" : "AUTO"; // width 是主軸
f.counterAxisSizingMode = "AUTO";
} else {
f.counterAxisSizingMode = width ? "FIXED" : "AUTO"; // width 是副軸
f.primaryAxisSizingMode = "AUTO";
}
在水平框架上設定 counterAxisSizingMode 會固定其高度,並讓寬度擁抱內容。以這種方式建立的每一行都沒有多餘的寬度,因此沒有任何子項目可以擴展進去。
在設定大小模式之前先調整大小,否則明確的大小會與擁抱內容產生衝突。
填滿一行的欄
function growCol(n) { n.layoutGrow = 1; n.counterAxisSizingMode = "FIXED"; return n; }
一個以 width: null 建立的垂直框架會擁抱內容。給它 layoutGrow = 1 和 STRETCH 子項目,你就會得到一個循環約束:父項目根據子項目調整大小,子項目根據父項目調整大小。Figma 會以最小值來解決它。
症狀: 卡片約 45px 寬,正文每行只有一個字元,文字節點報告 1 x 988。頁面框架比應有的高三倍,完全來自於虛假的垂直空間。
偏好將明確的寬度傳遞給行,而不是依賴 STRETCH 來解決。
測量,不要用看的
幾何數據回答問題的速度比截圖更快,而且它會揭露上述的所有錯誤。拉取 get_metadata,然後:
- 高度 ≤ 12px → 文字或框架崩潰
- 寬度為 1 → 上述的循環約束
- 頁面比其 HTML 對應物高出許多 → 狹窄的換行,而不是內容
- 預期有兩個時只有一個框架副本 → 程式從未執行過
對總數進行合理性檢查:一個區塊的 Y 軸範圍應該鋪滿其頁面且沒有間隙,最後一個區塊應該正好結束於頁面高度。
在本機模擬外掛程式
此技能中的 tools/figma_stub.js 是一個嚴格的模擬樁;node tools/test_figma_stub.js 證明了它在 Figma 會拋出錯誤的地方仍然會拋出。
一個 figma 全域物件的模擬樁可以捕捉拋出錯誤、null 附加和結構性錯誤,然後再進行往返測試。它無法模擬文字度量,所以它只能證明程式碼可以執行,永遠無法證明佈局是正確的。
讓模擬樁與真實 API 一樣嚴格,否則它會讓那些在 Figma 中會失敗的建置通過。以下是因為模擬樁過於寬容而最終出現在畫布上的錯誤:
resize(w, h){
if (!(w >= 0)) throw new Error('in resize: Property "width" failed validation');
if (!(h >= 0)) throw new Error('in resize: Property "height" failed validation');
...
}
set fontName(v){
if (!loaded.has(v.family + "|" + v.style))
throw new Error('Cannot use unloaded font "' + v.family + " " + v.style + '"');
...
}
appendChild(c){ if (!c) throw new Error("null child"); ... }
為模擬樁提供 currentPage.children,並用它來植入真實頁面已經擁有的框架——這就是原點衝突如何在本機上顯現出來。
為了檢查定位,模擬樁還需要計算自動佈局高度:對於垂直框架,將子項目的高度加上 itemSpacing 和內邊距;對於水平框架,取最大值;當 primaryAxisSizingMode === "FIXED" 時,返回固定值。粗略的計算足以捕捉行重疊。
執行每一個調度路徑、每一個中斷點和每一種字型設定,而不只是快樂路徑:
僅 Inter · Inter + mono · 全部三種 · 僅 Inter Regular
desktop · mobile dark · light
字型:載入你會要求的字型,並解析你得到的結果
分配一個未載入的字型會拋出錯誤。背後有兩個陷阱:
- 你必須預先載入程式碼要求的每一種字重。 載入 Regular、Medium 和 Bold,然後請求 Semi Bold 就是一個崩潰,只差一個使用它的卡片。
- 樣式名稱因字型家族而異。 Inter 使用
"Semi Bold";JetBrains Mono 使用"SemiBold"。以錯誤的拼寫請求正確的字重仍然會拋出錯誤。
單獨探測每一種字重,記錄實際上載入了什麼,並透過一個備援鏈來解析每個請求,而不是假設:
const ALIAS = { "Semi Bold": ["Semi Bold","SemiBold","Medium","Bold","Regular"], … };
function font(family, style){
for (const s of ALIAS[style]) if (LOADED[family+"|"+s]) return { family, style:s };
for (const s of ALIAS[style]) if (LOADED["Inter|"+s]) return { family:"Inter", style:s };
return { family:"Inter", style:"Regular" };
}
永遠不要嚴格地在入口點使用 Promise.all 進行預載入——那邊一個遺失的字重會殺死一個解析器原本可以處理的建置。在關閉通知訊息中報告你解析到的字型家族。
一個外掛,一件事
避免在單一外掛程式內部進行根據編輯器類型、頁面名稱或選取項目的調度開關。一個頁面名稱的開關意味著使用者必須在東西能運作之前執行某個儀式,而當它無聲地做了錯誤的事情時,沒有任何東西可以讀取。
兩個帶有不同 id 和 name 值的 manifest 成本為零,並且消除了整個類別的問題。當一個重新匯入的 manifest 帶有一個它已經知道的 id 時,Figma 也會重複使用一個快取的註冊,所以更改 id 是你強制乾淨載入的方法。
讀取評論
MCP 暴露了 get_metadata、get_design_context、get_screenshot、get_figjam、use_figma——這些都無法讀取評論,而且外掛程式 API 也沒有評論存取權限。評論存在於 REST API 中。
使用此技能資料夾中的 tools/figma_comments.py(Python 3,標準函式庫):
T=~/.claude/skills/figma-build/tools/figma_comments.py
python3 $T check <file-url> # OK: token 可以讀取評論 (檔案 KEY 上的 N 則)
python3 $T fetch <file-url> # 以 JSON 格式取得開啟的討論串,過濾掉已完成和已解決的
python3 $T mark <file-url> ID... --change "what changed"
python3 $T reply <file-url> ROOT_ID "Done: logo 20% bigger" # 需要寫入範圍
它會從 ~/.figma-token 讀取 token,並且永遠不會列印它。從工作資料夾執行它,以便記錄檔放在那裡,或者設定 FIGMA_LEDGER。沒有刪除指令。如果它報告 SSL 憑證失敗,表示 Python 沒有根憑證:改用 /usr/bin/python3 執行,或者 pip install certifi。
範圍:檔案評論 → 讀取。將 token 儲存在檔案中(~/.figma-token,chmod 600),永遠不要儲存在對話中。如果此人無法執行 shell 指令,請寫入檔案並加上佔位符號,然後使用 open -a TextEdit 開啟給他們貼上。
/v1/me 在僅有評論範圍的 token 上會回傳 403。這是預期行為,不是 token 壞掉。/v1/files/:key/nodes 在沒有 file_content:read 的情況下也會回傳 403。
每則評論都帶有 client_meta:
{ "node_id": "2:95", "node_offset": { "x": 279, "y": 1257 } }
透過拉取 get_metadata 並找出其 Y 軸範圍包含該偏移量的子框架,將其映射到一個區塊。討論串以扁平形式回傳——按 parent_id 分組,而回覆通常攜帶實際的指示,而根評論只是一個指標。
評論是基於檔案的。將內容移動到一個新檔案會把它們留在後面。
審查迴圈,當被要求「讀取我的評論」時
- 讀取:使用
figma_comments.py fetch,它會將評論分組為討論串。跳過已解決的(有設定resolved_at)以及所有已經在已處理記錄檔(如下)中的 ID。舊的評論會留在檔案中直到被解決,所以如果不使用這兩個過濾器,每個審查以及之後的每個對話都會重新套用上一輪的回饋。 - 映射:如上所述,透過
client_meta將每個討論串映射到其框架。 - 確認後再繪製: 在一個簡短的表格中列出每則評論、它映射到的框架以及計畫的更改。一則釘在空白畫布上或含糊不清的評論,會得到一個問題而不是猜測。
- 就地套用更改:更改現有節點的屬性,保持它們的 ID。永遠不要移除並重繪一個有評論的框架,因為這會使其評論成為孤兒。透過 MCP(
use_figma)可以直接更新畫布。透過開發外掛程式,請在磁碟上編輯其程式碼,增加建置戳記,然後請此人再次執行外掛程式。 - 驗證:使用
get_metadata和截圖(請參閱「測量,不要用看的」)。對於外掛程式的路徑,在相信畫布之前,請檢查關閉通知訊息是否顯示新的戳記。 - 回報:逐條評論回報更改了什麼。如果 token 具有寫入範圍並且此人希望如此,請在每個討論串中回覆。
- 記錄:在回合結束前,使用
figma_comments.py mark記錄每個已處理的評論 ID(討論串根和回覆),包括那些你僅以問題回答的。 - 永遠不要刪除評論,即使被要求清理。它們是審查記錄。舊的回饋是透過記錄檔而不是刪除來排除在後續回合之外的。如果一個回合需要在 Figma 中清除,請由此人解決評論。
已處理評論記錄檔。 一個 JSON 檔案,位於工作目錄旁邊:在活動資料夾中為 figma/comments-handled.json,否則為工作目錄中的 figma-comments-handled.json。在每次審查開始時讀取它,包括在新的對話中。
{ "<file_key>": { "<comment_id>": { "handled_at": "2026-11-03T14:05:00Z", "change": "logo 20% bigger on 1080x1920" } } }
沒有用於解決評論的 API 端點,因此解決評論的操作由此人完成。
何時要求重新匯入(僅限外掛程式路徑):當通知訊息顯示過時的戳記,或畫布上顯示框架時顯示「replaced 0」。請他們移除外掛程式,再次匯入其 manifest.json 並重新開啟分頁。如果仍然失敗,請更改 manifest 的 id(請參閱「一個外掛,一件事」)。
大規模截圖
每個頁面一次捕捉加上本機裁切,勝過每個區塊一次呼叫。區塊的 Y 軸/高度來自 get_metadata,設定 maxDimension 等於或高於框架高度可以得到 1:1 的截圖,而且裁切會完全對齊。