跳转到内容
搜索文档

直接上传

最后更新 查看 MarkdownAgent 设置

我们的 API 使用户能够上传静态资产并将其作为 Worker 的一部分。这些静态资产可免费提供服务;此外,用户还可通过可选的资产绑定(binding)获取资产,以支持更高级的应用。本指南说明如何通过 API 将资产直接附加到 Worker。

sequenceDiagram
    participant User
    participant Workers API
    User<<->>Workers API: Submit manifest<br/>POST /client/v4/accounts/:accountId/workers/scripts/:scriptName/assets-upload-session
    User<<->>Workers API: Upload files<br/>POST /client/v4/accounts/:accountId/workers/assets/upload?base64=true
    User<<->>Workers API: Upload script version<br/>PUT /client/v4/accounts/:accountId/workers/scripts/:scriptName
sequenceDiagram
    participant User
    participant Workers API
    User<<->>Workers API: Submit manifest<br/>POST /client/v4/accounts/:accountId/workers/dispatch/namespaces/:dispatchNamespace/scripts/:scriptName/assets-upload-session
    User<<->>Workers API: Upload files<br/>POST /client/v4/accounts/:accountId/workers/assets/upload?base64=true
    User<<->>Workers API: Upload script version<br/>PUT /client/v4/accounts/:accountId/workers/dispatch/namespaces/:dispatchNamespace/scripts/:scriptName

资产上传流程可归纳为三个阶段:

  1. 注册 manifest
  2. 上传资产
  3. 部署 Worker

上传 manifest

资产 manifest 是一份账本,记录我们希望在 Worker 中使用的文件。该 manifest 用于跟踪每个 Worker 版本关联的资产,并避免在新上传前重复上传未变更的文件。

manifest 上传请求描述我们打算上传的每个文件。每个文件对应一个键,表示文件路径和名称,其值为包含文件元数据的对象。

hash 表示文件的 32 位十六进制字符哈希,size 表示文件大小(字节)。

curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/assets-upload-session \
--header 'content-type: application/json' \
--header 'Authorization: Bearer <API_TOKEN>' \
--data '{
  "manifest": {
		"/filea.html": {
			"hash": "08f1dfda4574284ab3c21666d1",
			"size": 12
		},
		"/fileb.html": {
			"hash": "4f1c1af44620d531446ceef93f",
			"size": 23
		},
		"/filec.html": {
			"hash": "54995e302614e0523757a04ec1",
			"size": 23
		}
	}
}'
curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/dispatch/namespaces/{dispatch_namespace}/scripts/{script_name}/assets-upload-session \
--header 'content-type: application/json' \
--header 'Authorization: Bearer <API_TOKEN>' \
--data '{
  "manifest": {
		"/filea.html": {
			"hash": "08f1dfda4574284ab3c21666d1",
			"size": 12
		},
		"/fileb.html": {
			"hash": "4f1c1af44620d531446ceef93f",
			"size": 23
		},
		"/filec.html": {
			"hash": "54995e302614e0523757a04ec1",
			"size": 23
		}
	}
}'

响应会包含 JWT,用于文件上传时的身份验证。JWT 有效期为一小时。

除 JWT 外,响应还会说明如何最优地批量上传文件。这些说明编码在 buckets 字段中。buckets 中的每个数组包含应一起上传的文件哈希列表。若文件在 Worker 先前版本中最近已上传且未修改,则不会出现在 buckets 字段中(无需重新上传)。

{
	"result": {
		"jwt": "<UPLOAD_TOKEN>",
		"buckets": [
			["08f1dfda4574284ab3c21666d1", "4f1c1af44620d531446ceef93f"],
			["54995e302614e0523757a04ec1"]
		]
	},
	"success": true,
	"errors": null,
	"messages": null
}

限制

  • 限制因账户套餐而异。有关静态资产限制的更多信息,请参阅账户套餐限制

上传静态资产

文件上传 API要求使用 multipart/form-data 上传文件。每个文件的内容必须进行 base64 编码,且 URL 中的 base64 查询参数须设为 true

每个文件部分的 Content-Type 标头会在最终提供文件时附加。若部署时不想发送 Content-Type 标头,可在上传时使用 application/null

Authorization 标头须以 bearer token 形式提供,使用前述 manifest 上传调用返回的 JWT(上传令牌)。

manifest 中的每个文件上传完成后,将返回状态码 201,并包含 jwt 字段。该 JWT 为最终「完成」令牌,可用于创建包含此资产集的 Worker 部署。完成令牌有效期为 1 小时。

创建/部署新版本

ScriptVersionWorkers for Platform script 上传端点要求在表单数据中指定 metadata 部分。此处可提供上一步(上传资产)的完成令牌。

Example Worker Metadata Specifying Completion Tokenbash
{
  "main_module": "main.js",
  "assets": {
    "jwt": "<completion_token>"
  },
  "compatibility_date": "2021-09-14"
}

若 Worker 已有资产且仅希望复用现有资产集,则无需再次指定完成令牌,可传递布尔值 keep_assets 选项。

Example Worker Metadata Specifying keep_assetsbash
{
  "main_module": "main.js",
  "keep_assets": true,
  "compatibility_date": "2021-09-14"
}

资产路由配置可在 assets 对象中提供,例如 html_handlingnot_found_handling

Example Worker Metadata Specifying Asset Configurationbash
{
  "main_module": "main.js",
  "assets": {
    "jwt": "<completion_token>",
    "config" {
      "html_handling": "auto-trailing-slash"
    }
  },
  "compatibility_date": "2021-09-14"
}

若希望在 Worker 代码中获取并提供资产,可选提供资产绑定(binding)。

Example Worker Metadata Specifying Asset Bindingbash
{
  "main_module": "main.js",
  "assets": {
    ...
  },
  "bindings": [
    ...
    {
      "name": "ASSETS",
      "type": "assets"
    }
    ...
  ]
  "compatibility_date": "2021-09-14"
}

程序化示例

此示例来自 cloudflare-typescript

#!/usr/bin/env -S npm run tsn -T

/**
 * Create a Worker that serves static assets
 *
 * This example demonstrates how to:
 * - Upload static assets to Cloudflare Workers
 * - Create and deploy a Worker that serves those assets
 *
 * Docs:
 * - https://developers.cloudflare.com/workers/static-assets/direct-upload
 *
 * Prerequisites:
 * 1. Generate an API token: https://developers.cloudflare.com/fundamentals/api/get-started/create-token/
 * 2. Find your account ID: https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/
 * 3. Find your workers.dev subdomain: https://developers.cloudflare.com/workers/configuration/routing/workers-dev/
 *
 * Environment variables:
 *   - CLOUDFLARE_API_TOKEN (required)
 *   - CLOUDFLARE_ACCOUNT_ID (required)
 *   - ASSETS_DIRECTORY (required)
 *   - CLOUDFLARE_SUBDOMAIN (optional)
 *
 * Usage:
 *   Place your static files in the ASSETS_DIRECTORY, then run this script.
 *   Assets will be available at: my-script-with-assets.$subdomain.workers.dev/$filename
 */

import crypto from "crypto";
import fs from "fs";
import { readFile } from "node:fs/promises";
import { extname } from "node:path";
import path from "path";
import { exit } from "node:process";

import Cloudflare from "cloudflare";

const WORKER_NAME = "my-worker-with-assets";
const SCRIPT_FILENAME = `${WORKER_NAME}.mjs`;

function loadConfig() {
	const apiToken = process.env["CLOUDFLARE_API_TOKEN"];
	if (!apiToken) {
		throw new Error(
			"Missing required environment variable: CLOUDFLARE_API_TOKEN",
		);
	}

	const accountId = process.env["CLOUDFLARE_ACCOUNT_ID"];
	if (!accountId) {
		throw new Error(
			"Missing required environment variable: CLOUDFLARE_ACCOUNT_ID",
		);
	}

	const assetsDirectory = process.env["ASSETS_DIRECTORY"];
	if (!assetsDirectory) {
		throw new Error("Missing required environment variable: ASSETS_DIRECTORY");
	}

	if (!fs.existsSync(assetsDirectory)) {
		throw new Error(`Assets directory does not exist: ${assetsDirectory}`);
	}

	const subdomain = process.env["CLOUDFLARE_SUBDOMAIN"];

	return {
		apiToken,
		accountId,
		assetsDirectory,
		subdomain: subdomain || undefined,
		workerName: WORKER_NAME,
	};
}

const config = loadConfig();
const client = new Cloudflare({
	apiToken: config.apiToken,
});

/**
 * Recursively reads all files from a directory and creates a manifest
 * mapping file paths to their hash and size.
 */
function createManifest(directory) {
	const manifest = {};

	function processDirectory(currentDir, basePath = "") {
		try {
			const entries = fs.readdirSync(currentDir, { withFileTypes: true });

			for (const entry of entries) {
				const fullPath = path.join(currentDir, entry.name);
				const relativePath = path.join(basePath, entry.name);

				if (entry.isDirectory()) {
					processDirectory(fullPath, relativePath);
				} else if (entry.isFile()) {
					try {
						const fileContent = fs.readFileSync(fullPath);
						const extension = extname(relativePath).substring(1);

						// Generate a hash for the file
						const hash = crypto
							.createHash("sha256")
							.update(fileContent.toString("base64") + extension)
							.digest("hex")
							.slice(0, 32);

						// Normalize path separators to forward slashes
						const manifestPath = `/${relativePath.replace(/\\/g, "/")}`;

						manifest[manifestPath] = {
							hash,
							size: fileContent.length,
						};

						console.log(
							`Added to manifest: ${manifestPath} (${fileContent.length} bytes)`,
						);
					} catch (error) {
						console.warn(`Failed to process file ${fullPath}:`, error);
					}
				}
			}
		} catch (error) {
			throw new Error(`Failed to read directory ${currentDir}: ${error}`);
		}
	}

	processDirectory(directory);

	if (Object.keys(manifest).length === 0) {
		throw new Error(`No files found in assets directory: ${directory}`);
	}

	console.log(`Created manifest with ${Object.keys(manifest).length} files`);
	return manifest;
}

/**
 * Generates the Worker script content that serves static assets
 */
function generateWorkerScript(exampleFile) {
	return `
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    
    // Serve a simple index page at the root
    if (url.pathname === '/') {
      return new Response(
        \`<!DOCTYPE html>
<html>
<head>
  <title>Static Assets Worker</title>
  <style>
    body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
    h1 { color: #f38020; }
    .asset-info { background: #f5f5f5; padding: 15px; border-radius: 5px; }
  </style>
</head>
<body>
  <h1>This Worker serves static assets!</h1>
  <div class="asset-info">
    <p><strong>To access your assets,</strong> add <code>/filename</code> to the URL.</p>
    <p>Try visiting <a href="\${url.origin}/${exampleFile}">/${exampleFile}</a></p>
  </div>
</body>
</html>\`,
        {
          status: 200,
          headers: { 'Content-Type': 'text/html' }
        }
      );
    }
    
    // Serve static assets for all other paths
    return env.ASSETS.fetch(request);
  }
};
  `.trim();
}

/**
 * Creates upload payloads from buckets and manifest
 */
async function createUploadPayloads(buckets, manifest, assetsDirectory) {
	const payloads = [];

	for (const bucket of buckets) {
		const payload = {};

		for (const hash of bucket) {
			// Find the file path for this hash
			const manifestEntry = Object.entries(manifest).find(
				([_, data]) => data.hash === hash,
			);

			if (!manifestEntry) {
				throw new Error(`Could not find file for hash: ${hash}`);
			}

			const [relativePath] = manifestEntry;
			const fullPath = path.join(assetsDirectory, relativePath);

			try {
				const fileContent = await readFile(fullPath);
				payload[hash] = fileContent.toString("base64");
				console.log(`Prepared for upload: ${relativePath}`);
			} catch (error) {
				throw new Error(`Failed to read file ${fullPath}: ${error}`);
			}
		}

		payloads.push(payload);
	}

	return payloads;
}

/**
 * Uploads asset payloads
 */
async function uploadAssets(payloads, uploadJwt, accountId) {
	let completionJwt;

	console.log(`Uploading ${payloads.length} payload(s)...`);

	for (let i = 0; i < payloads.length; i++) {
		const payload = payloads[i];
		console.log(`Uploading payload ${i + 1}/${payloads.length}...`);

		try {
			const response = await client.workers.assets.upload.create(
				{
					account_id: accountId,
					base64: true,
					body: payload,
				},
				{
					headers: { Authorization: `Bearer ${uploadJwt}` },
				},
			);

			if (response?.jwt) {
				completionJwt = response.jwt;
			}
		} catch (error) {
			throw new Error(`Failed to upload payload ${i + 1}: ${error}`);
		}
	}

	if (!completionJwt) {
		throw new Error("Upload completed but no completion JWT received");
	}

	console.log("✅ All assets uploaded successfully");
	return completionJwt;
}

async function main() {
	try {
		console.log(
			"🚀 Starting Worker creation and deployment with static assets...",
		);
		console.log(`📁 Assets directory: ${config.assetsDirectory}`);

		console.log("📝 Creating asset manifest...");
		const manifest = createManifest(config.assetsDirectory);
		const exampleFile =
			Object.keys(manifest)[0]?.replace(/^\//, "") || "file.txt";

		const scriptContent = generateWorkerScript(exampleFile);

		let worker;
		try {
			worker = await client.workers.beta.workers.get(config.workerName, {
				account_id: config.accountId,
			});
			console.log(`♻️  Worker ${config.workerName} already exists. Using it.`);
		} catch (error) {
			if (!(error instanceof Cloudflare.NotFoundError)) {
				throw error;
			}
			console.log(`✏️  Creating Worker ${config.workerName}...`);
			worker = await client.workers.beta.workers.create({
				account_id: config.accountId,
				name: config.workerName,
				subdomain: {
					enabled: config.subdomain !== undefined,
				},
				observability: {
					enabled: true,
				},
			});
		}

		console.log(`⚙️  Worker id: ${worker.id}`);
		console.log("🔄 Starting asset upload session...");

		const uploadResponse = await client.workers.scripts.assets.upload.create(
			config.workerName,
			{
				account_id: config.accountId,
				manifest,
			},
		);

		const { buckets, jwt: uploadJwt } = uploadResponse;

		if (!uploadJwt || !buckets) {
			throw new Error("Failed to start asset upload session");
		}

		let completionJwt;

		if (buckets.length === 0) {
			console.log("✅ No new assets to upload!");
			// Use the initial upload JWT as completion JWT when no uploads are needed
			completionJwt = uploadJwt;
		} else {
			const payloads = await createUploadPayloads(
				buckets,
				manifest,
				config.assetsDirectory,
			);

			completionJwt = await uploadAssets(payloads, uploadJwt, config.accountId);
		}

		console.log("✏️  Creating Worker version...");

		// Create a new version with assets
		const version = await client.workers.beta.workers.versions.create(
			worker.id,
			{
				account_id: config.accountId,
				main_module: SCRIPT_FILENAME,
				compatibility_date: new Date().toISOString().split("T")[0],
				bindings: [
					{
						type: "assets",
						name: "ASSETS",
					},
				],
				assets: {
					jwt: completionJwt,
				},
				modules: [
					{
						name: SCRIPT_FILENAME,
						content_type: "application/javascript+module",
						content_base64: Buffer.from(scriptContent).toString("base64"),
					},
				],
			},
		);

		console.log("🚚 Creating Worker deployment...");

		// Create a deployment and point all traffic to the version we created
		await client.workers.scripts.deployments.create(config.workerName, {
			account_id: config.accountId,
			strategy: "percentage",
			versions: [
				{
					percentage: 100,
					version_id: version.id,
				},
			],
		});

		console.log("✅ Deployment successful!");

		if (config.subdomain) {
			console.log(`
🌍 Your Worker is live!
📍 Base URL: https://${config.workerName}.${config.subdomain}.workers.dev/
📄 Try accessing: https://${config.workerName}.${config.subdomain}.workers.dev/${exampleFile}
`);
		} else {
			console.log(`
⚠️  Set up a route, custom domain, or workers.dev subdomain to access your Worker.
Add CLOUDFLARE_SUBDOMAIN to your environment variables to set one up automatically.
`);
		}
	} catch (error) {
		console.error("❌ Deployment failed:", error);
		exit(1);
	}
}

main();
#!/usr/bin/env -S npm run tsn -T

/**
 * Create a Worker that serves static assets
 * 
 * This example demonstrates how to:
 * - Upload static assets to Cloudflare Workers
 * - Create and deploy a Worker that serves those assets
 * 
 * Docs:
 * - https://developers.cloudflare.com/workers/static-assets/direct-upload
 *
 * Prerequisites:
 * 1. Generate an API token: https://developers.cloudflare.com/fundamentals/api/get-started/create-token/
 * 2. Find your account ID: https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/
 * 3. Find your workers.dev subdomain: https://developers.cloudflare.com/workers/configuration/routing/workers-dev/
 *
 * Environment variables:
 *   - CLOUDFLARE_API_TOKEN (required)
 *   - CLOUDFLARE_ACCOUNT_ID (required) 
 *   - ASSETS_DIRECTORY (required)
 *   - CLOUDFLARE_SUBDOMAIN (optional)
 *
 * Usage:
 *   Place your static files in the ASSETS_DIRECTORY, then run this script.
 *   Assets will be available at: my-script-with-assets.$subdomain.workers.dev/$filename
 */

import crypto from 'crypto';
import fs from 'fs';
import { readFile } from 'node:fs/promises';
import { extname } from 'node:path';
import path from 'path';
import { exit } from 'node:process';

import Cloudflare from 'cloudflare';

interface Config {
  apiToken: string;
  accountId: string;
  assetsDirectory: string;
  subdomain: string | undefined;
  workerName: string;
}

interface AssetManifest {
  [path: string]: {
    hash: string;
    size: number;
  };
}

interface UploadPayload {
  [hash: string]: string; // base64 encoded content
}

const WORKER_NAME = 'my-worker-with-assets';
const SCRIPT_FILENAME = `${WORKER_NAME}.mjs`;

function loadConfig(): Config {
  const apiToken = process.env['CLOUDFLARE_API_TOKEN'];
  if (!apiToken) {
    throw new Error('Missing required environment variable: CLOUDFLARE_API_TOKEN');
  }

  const accountId = process.env['CLOUDFLARE_ACCOUNT_ID'];
  if (!accountId) {
    throw new Error('Missing required environment variable: CLOUDFLARE_ACCOUNT_ID');
  }

  const assetsDirectory = process.env['ASSETS_DIRECTORY'];
  if (!assetsDirectory) {
    throw new Error('Missing required environment variable: ASSETS_DIRECTORY');
  }

  if (!fs.existsSync(assetsDirectory)) {
    throw new Error(`Assets directory does not exist: ${assetsDirectory}`);
  }

  const subdomain = process.env['CLOUDFLARE_SUBDOMAIN'];

  return {
    apiToken,
    accountId,
    assetsDirectory,
    subdomain: subdomain || undefined,
    workerName: WORKER_NAME,
  };
}

const config = loadConfig();
const client = new Cloudflare({
  apiToken: config.apiToken,
});

/**
 * Recursively reads all files from a directory and creates a manifest
 * mapping file paths to their hash and size.
 */
function createManifest(directory: string): AssetManifest {
  const manifest: AssetManifest = {};
  
  function processDirectory(currentDir: string, basePath = ''): void {
    try {
      const entries = fs.readdirSync(currentDir, { withFileTypes: true });
      
      for (const entry of entries) {
        const fullPath = path.join(currentDir, entry.name);
        const relativePath = path.join(basePath, entry.name);

        if (entry.isDirectory()) {
          processDirectory(fullPath, relativePath);
        } else if (entry.isFile()) {
          try {
            const fileContent = fs.readFileSync(fullPath);
            const extension = extname(relativePath).substring(1);

            // Generate a hash for the file
            const hash = crypto
              .createHash('sha256')
              .update(fileContent.toString('base64') + extension)
              .digest('hex')
              .slice(0, 32);

            // Normalize path separators to forward slashes
            const manifestPath = `/${relativePath.replace(/\\/g, '/')}`;
            
            manifest[manifestPath] = {
              hash,
              size: fileContent.length,
            };
            
            console.log(`Added to manifest: ${manifestPath} (${fileContent.length} bytes)`);
          } catch (error) {
            console.warn(`Failed to process file ${fullPath}:`, error);
          }
        }
      }
    } catch (error) {
      throw new Error(`Failed to read directory ${currentDir}: ${error}`);
    }
  }
  
  processDirectory(directory);
  
  if (Object.keys(manifest).length === 0) {
    throw new Error(`No files found in assets directory: ${directory}`);
  }
  
  console.log(`Created manifest with ${Object.keys(manifest).length} files`);
  return manifest;
}

/**
 * Generates the Worker script content that serves static assets
 */
function generateWorkerScript(exampleFile: string): string {
  return `
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    
    // Serve a simple index page at the root
    if (url.pathname === '/') {
      return new Response(
        \`<!DOCTYPE html>
<html>
<head>
  <title>Static Assets Worker</title>
  <style>
    body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
    h1 { color: #f38020; }
    .asset-info { background: #f5f5f5; padding: 15px; border-radius: 5px; }
  </style>
</head>
<body>
  <h1>This Worker serves static assets!</h1>
  <div class="asset-info">
    <p><strong>To access your assets,</strong> add <code>/filename</code> to the URL.</p>
    <p>Try visiting <a href="\${url.origin}/${exampleFile}">/${exampleFile}</a></p>
  </div>
</body>
</html>\`,
        {
          status: 200,
          headers: { 'Content-Type': 'text/html' }
        }
      );
    }
    
    // Serve static assets for all other paths
    return env.ASSETS.fetch(request);
  }
};
  `.trim();
}

/**
 * Creates upload payloads from buckets and manifest
 */
async function createUploadPayloads(
  buckets: string[][],
  manifest: AssetManifest,
  assetsDirectory: string
): Promise<UploadPayload[]> {
  const payloads: UploadPayload[] = [];
  
  for (const bucket of buckets) {
    const payload: UploadPayload = {};
    
    for (const hash of bucket) {
      // Find the file path for this hash
      const manifestEntry = Object.entries(manifest).find(
        ([_, data]) => data.hash === hash
      );
      
      if (!manifestEntry) {
        throw new Error(`Could not find file for hash: ${hash}`);
      }
      
      const [relativePath] = manifestEntry;
      const fullPath = path.join(assetsDirectory, relativePath);
      
      try {
        const fileContent = await readFile(fullPath);
        payload[hash] = fileContent.toString('base64');
        console.log(`Prepared for upload: ${relativePath}`);
      } catch (error) {
        throw new Error(`Failed to read file ${fullPath}: ${error}`);
      }
    }
    
    payloads.push(payload);
  }
  
  return payloads;
}

/**
 * Uploads asset payloads
 */
async function uploadAssets(
  payloads: UploadPayload[],
  uploadJwt: string,
  accountId: string
): Promise<string> {
  let completionJwt: string | undefined;
  
  console.log(`Uploading ${payloads.length} payload(s)...`);
  
  for (let i = 0; i < payloads.length; i++) {
    const payload = payloads[i]!;
    console.log(`Uploading payload ${i + 1}/${payloads.length}...`);
    
    try {
      const response = await client.workers.assets.upload.create(
        {
          account_id: accountId,
          base64: true,
          body: payload,
        },
        {
          headers: { Authorization: `Bearer ${uploadJwt}` },
        }
      );
      
      if (response?.jwt) {
        completionJwt = response.jwt;
      }
    } catch (error) {
      throw new Error(`Failed to upload payload ${i + 1}: ${error}`);
    }
  }
  
  if (!completionJwt) {
    throw new Error('Upload completed but no completion JWT received');
  }
  
  console.log('✅ All assets uploaded successfully');
  return completionJwt;
}

async function main(): Promise<void> {
  try {
    console.log('🚀 Starting Worker creation and deployment with static assets...');
    console.log(`📁 Assets directory: ${config.assetsDirectory}`);
    
    console.log('📝 Creating asset manifest...');
    const manifest = createManifest(config.assetsDirectory);
    const exampleFile = Object.keys(manifest)[0]?.replace(/^\//, '') || 'file.txt';

    const scriptContent = generateWorkerScript(exampleFile);
    
    let worker;
    try {
      worker = await client.workers.beta.workers.get(config.workerName, {
        account_id: config.accountId,
      });
      console.log(`♻️  Worker ${config.workerName} already exists. Using it.`);
    } catch (error) {
      if (!(error instanceof Cloudflare.NotFoundError)) { throw error; }
      console.log(`✏️  Creating Worker ${config.workerName}...`);
      worker = await client.workers.beta.workers.create({
        account_id: config.accountId,
        name: config.workerName,
        subdomain: {
          enabled: config.subdomain !== undefined,
        },
        observability: {
          enabled: true,
        },
      });
    }

    console.log(`⚙️  Worker id: ${worker.id}`);
    console.log('🔄 Starting asset upload session...');
    
    const uploadResponse = await client.workers.scripts.assets.upload.create(
      config.workerName,
      {
        account_id: config.accountId,
        manifest,
      }
    );
    
    const { buckets, jwt: uploadJwt } = uploadResponse;
    
    if (!uploadJwt || !buckets) {
      throw new Error('Failed to start asset upload session');
    }
    
    let completionJwt: string;
    
    if (buckets.length === 0) {
      console.log('✅ No new assets to upload!');
      // Use the initial upload JWT as completion JWT when no uploads are needed
      completionJwt = uploadJwt;
    } else {
      const payloads = await createUploadPayloads(
        buckets,
        manifest,
        config.assetsDirectory
      );
      
      completionJwt = await uploadAssets(
        payloads,
        uploadJwt,
        config.accountId
      );
    }

    console.log('✏️  Creating Worker version...');

    // Create a new version with assets
    const version = await client.workers.beta.workers.versions.create(worker.id, {
      account_id: config.accountId,
      main_module: SCRIPT_FILENAME,
      compatibility_date: new Date().toISOString().split('T')[0]!,
      bindings: [
        {
          type: 'assets',
          name: 'ASSETS',
        },
      ],
      assets: {
        jwt: completionJwt,
      },
      modules: [
        {
          name: SCRIPT_FILENAME,
          content_type: 'application/javascript+module',
          content_base64: Buffer.from(scriptContent).toString('base64'),
        },
      ],
    });
    
    console.log('🚚 Creating Worker deployment...');
    
    // Create a deployment and point all traffic to the version we created
    await client.workers.scripts.deployments.create(config.workerName, {
      account_id: config.accountId,
      strategy: 'percentage',
      versions: [
        {
            percentage: 100,
            version_id: version.id,
          },
        ],
    });

    console.log('✅ Deployment successful!');
    
    if (config.subdomain) {
      console.log(`
🌍 Your Worker is live!
📍 Base URL: https://${config.workerName}.${config.subdomain}.workers.dev/
📄 Try accessing: https://${config.workerName}.${config.subdomain}.workers.dev/${exampleFile}
`);
    } else {
      console.log(`
⚠️  Set up a route, custom domain, or workers.dev subdomain to access your Worker.
Add CLOUDFLARE_SUBDOMAIN to your environment variables to set one up automatically.
`);
    }
  } catch (error) {
    console.error('❌ Deployment failed:', error);
    exit(1);
  }
}

main();

这篇文档对您有帮助吗?