部署到 Cloudflare

本頁內容

本指南將 EmDash 網站部署到 Cloudflare Workers,使用 D1 作為資料庫、R2 作為媒體儲存。可從 EmDash Cloudflare 範本開始,或將相同設定套用到現有 Astro 網站。

先決條件

  • Cloudflare 帳號
  • 已安裝專案相依套件
  • Wrangler 已通過 Cloudflare 驗證(pnpm wrangler login)

設定繫結

Cloudflare 範本包含完整的 Worker 進入點,以及具名的 D1 與 R2 繫結。首次部署時,若設定的名稱尚不存在,Wrangler 會建立相應資源。請保留 wrangler.jsonc 中的名稱;Wrangler 會將後續部署重新連接到相同資源。

範本使用以下繫結:

{
	"$schema": "node_modules/wrangler/config-schema.json",
	"name": "my-emdash-site",
	"main": "./src/worker.ts",
	"compatibility_date": "2026-02-24",
	"compatibility_flags": ["nodejs_compat"],

	"d1_databases": [
		{
			"binding": "DB",
			"database_name": "my-emdash-site",
		},
	],

	"r2_buckets": [
		{
			"binding": "MEDIA",
			"bucket_name": "my-emdash-media",
		},
	],
	"worker_loaders": [{ "binding": "LOADER" }],
	"triggers": { "crons": ["* * * * *"] },
}

DB、MEDIA 和 LOADER 名稱必須與 EmDash 配接器相符。Cron Trigger 會執行排程發布、外掛任務、備份與維護。若網站使用沙箱外掛,請參閱 Plugin sandbox。

設定 EmDash

以下 Astro 設定使用 D1 與 R2 繫結。

import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import react from "@astrojs/react";
import emdash from "emdash/astro";
import { d1, r2, sandbox } from "@emdash-cms/cloudflare";

export default defineConfig({
	output: "server",
	adapter: cloudflare(),
	integrations: [
		react(), // Required — the admin UI is a React app
		emdash({
			database: d1({ binding: "DB" }),
			storage: r2({ binding: "MEDIA" }),
			sandboxRunner: sandbox(),
		}),
	],
});

若網站不使用 marketplace、registry 或 sandboxed 外掛,可省略 sandboxRunner 與 LOADER 繫結。

新增 Worker 進入點

Worker 進入點會將 Astro 連接到 Cron Trigger,並匯出外掛橋接:

import handler, { createScheduledHandler, PluginBridge } from "@emdash-cms/cloudflare/worker";

export { PluginBridge };

export default {
	...handler,
	scheduled: createScheduledHandler(),
} satisfies ExportedHandler;

未安裝沙箱外掛時,匯出 PluginBridge 不會造成任何影響。若同一專案稍後可能啟用外掛,請保留它。

若要以非每分鐘一次的排程執行一般維護,請將同一個 Cron 運算式傳給 createScheduledHandler({ generalCron: "..." }) 和 triggers.crons。若兩者不同,處理常式會記錄並忽略非預期的觸發。

建置與部署

建置並部署網站一次,讓 Wrangler 佈建具名的 D1 資料庫與 R2 儲存貯體。Wrangler 使用 pnpm wrangler login 建立的本機登入。

pnpm build
pnpm wrangler deploy

在預設的 auto 遷移模式下,已部署的 Worker 收到第一個請求時,EmDash 會套用待處理的核心遷移。當部署管線必須在新程式碼接收流量之前套用遷移,或需要檢查、核對或復原遷移時,請使用 Manage core database migrations。

在設定精靈完成之前,EmDash 還會在第一個請求時套用一次 seed 檔案的 schema 與結構。只有當管理員在設定精靈中選擇時,才會套用 seed 中的範例內容。Seed 會在建置時從 .emdash/seed.json、package.json#emdash.seed 中的路徑或 seed/seed.json(以先找到的為準)讀取,並內嵌到套件中。若都不存在,則使用內建的預設 seed。對既有資料庫的後續部署不會變動其內容。

若要變更已部署網站的 schema 或內容模型,請參閱 Evolving a Deployed Site。

排程發布

在 Cloudflare 上,EmDash 只會透過 Worker 的 scheduled 處理常式發布已排程的項目,Cloudflare 會依照 Cron Trigger 的排程呼叫該處理常式。已部署的 Worker 需要同時具備以下兩項:

  • wrangler.jsonc 的 triggers.crons 清單中有一個 Cron Trigger,如設定繫結所示。
  • 來自 @emdash-cms/cloudflare/worker 的 scheduled 處理常式,從 main 指向的 Worker 進入點匯出,如新增 Worker 進入點所示。

若缺少其中任何一項,已排程的項目都不會上線。在使用 astro dev 的本機開發中,EmDash 會以計時器發布已排程的項目,因此在本機可行的排程並不能證明已部署的設定正確。

在專案根目錄執行 emdash doctor,檢查本機專案檔案中的 Cron Trigger 與 scheduled 處理常式:

pnpm emdash doctor

當 emdash doctor 同時找到兩者時,輸出會包含一項通過的 scheduler wiring 檢查。失敗的排程器檢查會描述問題並印出修正方式。該檢查能辨識本頁所示的 Worker 進入點,以及它作為修正方式印出的單行重新匯出,因此以其他方式撰寫的進入點即使能正常運作,也可能無法通過該檢查。

若沒有出現排程器檢查,表示 emdash doctor 在目前目錄中找不到 Wrangler 設定,或是既找不到 Cron Trigger 也找不到 scheduled 處理常式。在 D1 專案上,database 檢查會失敗,因為它尋找的是本機 SQLite 檔案(預設為 ./data.db)。該失敗不影響排程器檢查。

新增或變更 Cron Trigger 或 scheduled 處理常式後,請重新部署。Cloudflare 最多可能需要 15 分鐘才會開始執行新的或已變更的 Cron Trigger。觸發器開始執行後,EmDash 會發布排程時間已過的項目。

若要觀察觸發器,請執行 pnpm wrangler tail。每次執行都會印出類似 "* * * * *" @ 9/29/2026, 10:01:00 AM - Ok 的一行。Ok 表示該次執行結束時沒有未捕捉的錯誤。EmDash 會捕捉並記錄發布錯誤,因此 Ok 並不能確認項目已發布。發布了項目的執行還會記錄類似 [scheduled] Published 1 scheduled item(s) 的訊息,該次執行的錯誤會出現在同一份輸出中。

部署後,當某個已排程的項目已超過其排程時間,且最近五分鐘內沒有完成任何排程器執行時,管理儀表板會顯示 Scheduled publishing needs attention。如果 Cron Trigger 的執行頻率低於每五分鐘一次,該提示也可能在兩次執行之間出現,直到下一次執行發布該項目。

將 Worker 放在靠近 D1 的位置

Cloudflare 預設在靠近訪客處執行 Worker。EmDash 的伺服器端轉譯請求會多次往返 D1,因此請使用 Targeted Placement,在靠近 D1 主要資料庫處執行 Worker,以加快這些請求。

Wrangler 接受恰好帶有一個選擇器的 placement.mode: "targeted":region、host 或 hostname。請選擇指向 D1 主要資料庫位置的值,並將得到的 placement 物件加入 wrangler.jsonc。請勿將 D1 唯讀複本與 Targeted Placement 一起啟用。請將 EmDash 的 session 設定保持預設值 "disabled",讓讀取與寫入使用附近的主要資料庫。

物件快取

為降低 D1 的讀取負載,可將內容與設定的查詢結果快取在 Cloudflare KV 中。讀取會由 KV 提供,而不是在每次請求時查詢資料庫:

import { d1, r2, kvCache } from "@emdash-cms/cloudflare";

emdash({
	database: d1({ binding: "DB" }),
	storage: r2({ binding: "MEDIA" }),
	objectCache: kvCache({ binding: "CACHE" }),
}),

KV 設定、選項與失效行為請參閱 Object Cache。

Workers Cache

Cloudflare 的 Workers Cache 會在 Worker 前面放置邊緣快取:相符的請求會在完全不執行 Worker 的情況下被提供。

啟用

  1. 使用 Astro 的 Cloudflare 快取提供者,讓路由規則與 Astro.cache 設定快取標頭,並讓失效使用 cache.purge()。

    import { cacheCloudflare } from "@astrojs/cloudflare/cache";
    
    export default defineConfig({
     adapter: cloudflare(),
     cache: {
       provider: cacheCloudflare(),
     },
     routeRules: {
       "/": { maxAge: 300, swr: 86400 },
       // Other public routes can use different cache lifetimes.
     },
    });

    @astrojs/cloudflare 配接器會偵測 cacheCloudflare(),並在產生的部署設定中啟用 Workers Cache。

  2. 使用平台 API 從 Worker 程式碼清除已快取的回應。此呼叫不需要 Cloudflare REST 憑證。

    import { cache } from "cloudflare:workers";
    
    await cache.purge({ purgeEverything: true });
    // Or purge selected tags:
    await cache.purge({ tags: ["posts"] });

EmDash 的管理與 API 回應已傳送 Cache-Control: private, no-store,且永遠不會被儲存。公開頁面透過 Cache-Control / routeRules / Astro.cache 控制自身的快取。

啟用前有兩點需要注意:

  1. 沒有 Cache-Control 標頭的回應仍會被快取。 Workers Cache 套用 RFC 9111 的啟發式新鮮度——沒有任何標頭的 200 會快取 2 小時。請為每個自訂路由明確設定 Cache-Control(對依賴工作階段的內容使用 private, no-store)。
  2. 快取頁面會與已登入的編輯者共用。 快取在 Worker 之前執行,因此無法根據請求 cookie 繞過。已登入的編輯者可能收到公開頁面的快取匿名版本——沒有視覺化編輯工具列——直到項目過期。編輯者轉譯的回應本身永遠不會被儲存(帶有 private, no-store),因此不會向另一個方向外洩。

Workers Caching 與物件快取(objectCache: kvCache({ binding: "CACHE" }))是分開的,後者在 Worker 之下的 KV 中快取資料庫查詢結果。

自訂網域

首次部署會取得 workers.dev URL。自訂網域必須已是與 Worker 位於同一帳號、由 Cloudflare 管理的有效網域。Worker 在其 workers.dev URL 成功回應後,將正式環境網域新增為 Wrangler 路由:

{
	"routes": [{ "pattern": "www.example.com", "custom_domain": true }],
}

再次部署並驗證兩個位址。在測試 DNS 時保持 workers.dev 位址可用,有助於區分路由問題與應用程式問題。若要讓新網域成為網站地圖和搜尋中繼資料中的位址,以及在未設定 siteUrl、EMDASH_SITE_URL 或 SITE_URL 時郵件和外掛程式中使用的位址,請使用 Settings > General 中的 Change domain;參見遷移到新網域。

如果沒有設定 siteUrl、EMDASH_SITE_URL 或 SITE_URL,設定會將其執行所在的位址記錄為網站的來源,設定期間建立的通行金鑰只能在該位址上使用。請在正式環境網域上執行設定,或在設定之前將 EMDASH_SITE_URL 設為該網域。設定之後若將 EMDASH_SITE_URL 改為另一個網域,登入會轉移到該網域,而在舊位址上建立的通行金鑰將無法在新網域上使用。

公開 R2 存取

預設情況下,媒體透過 EmDash 的已驗證媒體路由提供。若儲存貯體有公開的自訂網域,請將該來源設為 publicUrl,讓產生的媒體 URL 使用它:

storage: r2({
	binding: "MEDIA",
	publicUrl: "https://media.example.com",
}),

公開儲存貯體存取適用於每個可連到的物件,而不只是媒體。自動 JSON 備份在同一個儲存後端使用 backups/ 前綴,因此不要透過公開網域暴露該前綴。Choose media storage 說明了安全邊界。

影像轉換

EmDash 透過 Cloudflare 的 IMAGES 繫結,在 Worker 內調整大小並重新編碼 R2 媒體。emdash/ui 的 Image 元件與富文字中的影像,都會透過 EmDash 在 Cloudflare 配接器下安裝的影像端點轉譯。對於內部路由 /_emdash/api/media/file/… 上的媒體,該端點會直接從 R2 繫結讀取來源位元組,無需 HTTP 擷取。這些轉換在 Cloudflare Access 之後以及使用 global_fetch_strictly_public 時仍可運作。從儲存貯體 URL 提供的媒體——請參閱公開 R2 存取——則改用配接器自己的轉換端點,該端點會在轉換前透過 HTTP 擷取檔案。

你不必宣告該繫結。@astrojs/cloudflare 會在 astro build 期間將它加入所產生的 Worker 設定,方式與為 Workers Caching 加入 cache 相同。每當執行階段影像服務為 cloudflare-binding 時就會如此:imageService 未設定、為該字串本身,或為 { runtime: "cloudflare-binding" }。其他任何值——"passthrough"、"compile"、"cloudflare"、"custom"——都會省略該繫結。在自己的 wrangler.jsonc 中列出它,可讓意圖更明確:

{
	"images": {
		"binding": "IMAGES",
	},
}

若要查看部署實際取得什麼,請閱讀產生的設定,而不是 wrangler.jsonc。建置會寫入 .wrangler/deploy/config.json,它會將 wrangler deploy 指向合併後的檔案(預設為 dist/server/wrangler.json)。請在那裡尋找 images 項目。

Cloudflare 會將這些轉換依 Images transformations 計費。來源影像與參數的每個唯一組合,每個日曆月計費一次,該月內的重複請求免費。若網站有 500 張來源影像,且每張都請求一種縮圖尺寸和一種主圖尺寸,則這兩組參數在該月會計為 1,000 張轉換後的影像。Images Free 方案每月涵蓋 5,000 次唯一轉換。超過該上限後,已快取的轉換仍會提供,但新的轉換會回傳 9422 錯誤,影像請求隨之失敗。

Cloudflare Access 驗證

Cloudflare Access 可用附加到 Access 應用程式的身分提供者取代通行金鑰驗證。受眾值是機密的執行階段設定;請透過命名其環境變數,將它排除在 astro.config.mjs 之外:

import { access } from "@emdash-cms/cloudflare";

emdash({
	auth: access({
		teamDomain: "myteam.cloudflareaccess.com",
		audienceEnvVar: "CF_ACCESS_AUDIENCE",
		roleMapping: {
			Admins: 50,
			Editors: 40,
		},
	}),
}),

使用 pnpm wrangler secret put CF_ACCESS_AUDIENCE 設定 CF_ACCESS_AUDIENCE。authentication guide 說明使用者佈建、預設角色與角色同步。

電子郵件

正式環境的 Worker 沒有預設的電子郵件寄送服務。在電子郵件外掛啟用之前,魔術連結登入會回傳 Email is not configured。

Cloudflare 電子郵件外掛使用 send_email 繫結。請先透過 Cloudflare Email Sending 完成寄件者網域的註冊與驗證。Cloudflare 會拒絕 From 位址不是已接受寄件者的郵件。

新增繫結並註冊提供者:

{
	"send_email": [{ "name": "EMAIL" }],
}
import { cloudflareEmail } from "@emdash-cms/cloudflare/plugins";

emdash({
	plugins: [
		cloudflareEmail({
			from: { email: "[email protected]", name: "My Site CMS" },
			replyTo: "[email protected]",
		}),
	],
}),

該外掛自首次部署起即處於啟用狀態。Settings → Email 會顯示所選的提供者,並可寄送測試郵件。在寄件者被接受且繫結存在之前,寄送都會失敗。

除非其 binding 選項指定了其他名稱,否則外掛會使用名為 EMAIL 的繫結。若它是唯一啟用中的電子郵件提供者,EmDash 會自動選取它。若有多個提供者啟用,請在 Settings → Email 下選擇 Cloudflare 提供者。選用的 replyTo 位址可在不變更已接受之 From 位址的情況下接收回覆。外掛可為單一訊息設定 replyTo,這會覆寫該訊息的此選項。

AI Search 外掛需要原生外掛註冊與 ai_search_namespaces 繫結。部署後,在管理介面中開啟 Cloudflare AI Search,選擇 collection,並執行 Sync All Content。初次同步會為啟用外掛之前已發布的內容建立索引;hooks 會讓後續變更保持同步。

import { aiSearch } from "@emdash-cms/cloudflare/plugins";

emdash({
	plugins: [aiSearch()],
}),
{
	"ai_search_namespaces": [{ "binding": "AI_SEARCH", "namespace": "default" }],
}

從網站公開搜尋路由:

export { POST, prerender } from "@emdash-cms/cloudflare/plugins/ai-search";

將搜尋介面加入版面配置。觸發插槽接受與網站設計相符的按鈕:

---
import AISearchSnippet from "@emdash-cms/cloudflare/plugins/ai-search/astro";
---

<AISearchSnippet apiUrl="/api/ai-search" placeholder="Search content">
	<button slot="trigger" type="button">Search</button>
</AISearchSnippet>

Worker 密鑰

使用 pnpm wrangler secret put <NAME> 儲存密鑰值。請勿將它們放入 wrangler.jsonc,也不要從建置時的 import.meta.env 值讀取。

EMDASH_ENCRYPTION_KEY 會加密宣告為密鑰的外掛設定。請在儲存外掛密鑰之前設定它,並與 D1 備份分開保存。輪替期間,請先提供新金鑰,並在逗號後保留較舊的金鑰,直到每個外掛密鑰都已重新儲存。Secrets and key management 說明了輪替與復原。

外掛橋接會直接讀取此 Worker 密鑰繫結。產生的管理設定路由則透過 process.env 讀取它。使用 nodejs_compat 時,對於相容性日期為 2025-04-01 或之後的專案,Cloudflare 預設會填入 process.env。固定在更早日期的專案,在儲存加密設定之前還必須加入 nodejs_compat_populate_process_env。

EmDash 在執行階段從 process.env 讀取密鑰。Worker 程式碼則從 cloudflare:workers 匯入的 env 讀取繫結。切勿透過 import.meta.env 讀取密鑰:Vite 會在建置時取代這些值,並可能將它們寫入伺服器套件。

預覽 HMAC 密鑰與留言者 IP salt 除非你提供執行階段覆寫,否則會產生並儲存在資料庫中。Secrets and key management 列出確切的變數、儲存位置與輪替影響。

預覽部署

預覽既可以作為 Worker Preview 執行(它會在正式環境 Worker 上為每個分支提供獨立的 URL),也可以作為具名 Wrangler 環境中的獨立 Worker 執行。無論哪種方式,預覽都需要與正式環境分開的資料庫和儲存貯體。切勿將預覽繫結指向正式環境的資料庫或儲存貯體。

Worker Previews

Worker Previews 要求專案相依項目中的 Wrangler 為 4.135.0 或更新版本。由較舊的 EmDash 範本建立的專案可能鎖定了更早的版本;請使用 pnpm add -D wrangler@latest 升級。預覽不會繼承頂層設定中的繫結或 vars;請在 previews 區塊中列出預覽使用的每個繫結。

Astro Cloudflare 配接器會新增一個 SESSION KV 繫結。wrangler deploy 會自動佈建它,但 wrangler preview 在沒有命名空間 ID 時會拒絕它。請建立預覽資源,包括用於工作階段的 KV 命名空間:

pnpm wrangler d1 create my-emdash-site-preview
pnpm wrangler r2 bucket create my-emdash-media-preview
pnpm wrangler kv namespace create my-emdash-site-preview-session

將 Wrangler 輸出的資源識別碼新增到 wrangler.jsonc:

{
	"previews": {
		"d1_databases": [
			{
				"binding": "DB",
				"database_name": "my-emdash-site-preview",
				"database_id": "00000000-0000-0000-0000-000000000000",
			},
		],
		"r2_buckets": [
			{
				"binding": "MEDIA",
				"bucket_name": "my-emdash-media-preview",
			},
		],
		"kv_namespaces": [{ "binding": "SESSION", "id": "00000000000000000000000000000000" }],
		"worker_loaders": [{ "binding": "LOADER" }],
	},
}

當預覽使用 send_email、AI Search、vars 及其他功能時,請重複宣告這些繫結。

設定精靈和通行金鑰需要預覽的站台 URL。如果沒有 siteUrl 或 EMDASH_SITE_URL,設定會失敗並報 SITE_URL_REQUIRED。每次部署預覽時,都要傳入預覽 URL https://<preview-name>-<worker-name>.<subdomain>.workers.dev:

pnpm build
pnpm wrangler preview --name my-branch \
  --var EMDASH_SITE_URL:https://my-branch-my-emdash-site.example.workers.dev

請在 --name 和 URL 中使用相同的預覽名稱。astro.config.mjs 中的 siteUrl 會覆寫 EMDASH_SITE_URL,並被建置進每個預覽,因此預覽上的通行金鑰會繫結到正式環境網域,設定和登入在預覽上都無法使用。請改為在頂層 vars 中用 EMDASH_SITE_URL 設定正式環境 URL。

如果 Worker 將 workers_dev 設為 false,則在你於 wrangler.jsonc 頂層設定 "preview_urls": true 並執行一次 wrangler deploy 之前,預覽沒有 URL。wrangler preview 不會套用這項設定。wrangler deploy 會把目前簽出中的程式碼(而不只是這項設定)發佈到正式環境,因此請提交這項變更,並在正式環境分支的乾淨簽出中部署。然後回到你的分支,再次執行 wrangler preview。

用 pnpm wrangler preview secret put <NAME> --name my-branch 為某一個預覽設定密鑰。pnpm wrangler preview base-config secret put <NAME> 會為此後建立的預覽設定密鑰;既有的預覽保留它們原來的密鑰。

請留意以下與正式環境的差異:

  • 預覽共用 previews 區塊中的資源。 wrangler preview 讀取其所部署分支的 previews 區塊,因此所有沿用基礎分支設定的分支都使用同一個資料庫。對預覽的第一次請求會套用待處理的核心遷移,所以升級 EmDash 的分支也會遷移其他預覽所使用的資料庫。若要隔離某個分支,請再建立一個 D1 資料庫,並在部署前修改該分支 previews 區塊中的 database_name 和 database_id。
  • 空的預覽資料庫會顯示設定精靈。 預覽 URL 是公開的,第一個完成精靈的訪客會成為管理員。請在建立預覽後立即完成設定,或用 Cloudflare Access 保護預覽 URL。
  • Cron Triggers 不會執行。 排程文章不會發佈,外掛程式的 cron 任務和排程備份也不會在預覽上執行。
  • 通行金鑰只在註冊它的主機名稱上有效。 正式環境的通行金鑰在預覽上無效,在第一個預覽上建立的管理員只能在該預覽 URL 上用通行金鑰登入。在共用該資料庫的另一個預覽上,請使用魔法連結登入(它需要郵件提供方及其在 previews 區塊中的繫結),並在那裡新增通行金鑰。或者,如上所述為該分支單獨使用一個資料庫來隔離它,並在其上執行設定。

具名環境

具名的 Wrangler 環境不會繼承繫結。請建立獨立的預覽資源,並在建置前將它們寫入 preview 環境:

pnpm wrangler d1 create my-emdash-site-preview \
  --binding DB --env preview --update-config
pnpm wrangler r2 bucket create my-emdash-media-preview \
  --binding MEDIA --env preview --update-config

預覽環境必須重複預覽 Worker 使用的每個繫結。在 Wrangler 寫入資源識別碼後,核心 D1、R2 與沙箱繫結具有以下形式:

{
	"env": {
		"preview": {
			"d1_databases": [
				{
					"binding": "DB",
					"database_name": "my-emdash-site-preview",
					"database_id": "00000000-0000-0000-0000-000000000000",
				},
			],
			"r2_buckets": [
				{
					"binding": "MEDIA",
					"bucket_name": "my-emdash-media-preview",
				},
			],
			"worker_loaders": [{ "binding": "LOADER" }],
		},
	},
}

請使用 Wrangler 寫入的預覽 UUID。當預覽使用這些功能時,請重複選用的 KV、AI Search、電子郵件及其他繫結。使用 pnpm wrangler secret put <NAME> --env preview 新增僅限預覽的密鑰。

建置並部署預覽環境。其第一個請求會透過預設的 auto 模式套用待處理的核心遷移。

pnpm build
pnpm wrangler deploy --env preview

在分享之前,請驗證預覽 URL、管理登入、媒體上傳,以及任何選用繫結。

驗證部署

部署後,請求一個公開頁面,登入 /_emdash/admin,上傳並取回一個測試媒體檔案,並確認 scheduled 處理常式出現在 pnpm wrangler tail 中。

疑難排解

「D1 binding not found」

請確認 wrangler.jsonc 中的繫結名稱與資料庫設定相符:

// Must match: d1({ binding: "DB" })
"binding": "DB"

「R2 binding not found」

請檢查 R2 儲存貯體是否已正確繫結:

// Must match: r2({ binding: "MEDIA" })
"binding": "MEDIA"

遷移錯誤

若看到 schema 錯誤,請追蹤 Worker 日誌(wrangler tail)並重現錯誤以取得底層訊息——然後附上該輸出提交 issue。