以前在自己的服务器上用PHP代码做了一个接口,服务器过期后就失效了。最近接触到 Cloudflare Workers,因为不用部署在服务器上,而且支持 HTTPS,每天有免费的10万次请求数,于是又在 Cloudflare Workers 上做了一个。

以下步骤及代码均来源于阿里云·通义大模型:


要使用 Cloudflare Workers 搭建一个获取必应每日美图的 API,你可以按照以下步骤进行:

  1. 注册并登录 Cloudflare 账户

    • 如果还没有 Cloudflare 账户,首先需要注册一个。
  2. 创建一个新的 Worker

    • 登录 Cloudflare 控制台后,进入 Workers 部分,点击“Create a service”按钮。
    • 输入你的 Worker 名称,然后点击“Create service”。
  3. 编写 Worker 代码

    • 在 Worker 编辑器中,你可以编写 JavaScript 代码来实现获取必应每日美图的功能。
    • 必应每日美图的 URL 是 https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=en-US,你可以通过这个 URL 获取图片信息。
  4. 示例代码

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
// 假设的必应每日美图页面URL,实际上你需要用真实的URL替换它。
const bingDailyImageURL = 'https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN';

// 我们首先请求必应的API以获得今日美图的具体URL
const response = await fetch(bingDailyImageURL);
const data = await response.json();

// 构建完整的图片URL
const baseUrl = 'https://s.cn.bing.net';
const imageUrl = `${baseUrl}${data.images[0].url}`;

// 返回302重定向响应
return Response.redirect(imageUrl, 302);
}
  1. 部署 Worker

    • 编写完代码后,点击“Save and Deploy”按钮将你的 Worker 部署到 Cloudflare 的边缘网络。
  2. 测试 API

    • 部署完成后,你可以通过浏览器或者工具(如 Postman)访问你的 Worker URL 来测试 API 是否正常工作。
  3. 设置自定义域名(可选)

    • 如果你有自己的域名,可以在 Cloudflare 中添加域名,并将 Worker 关联到这个域名上,以便通过自定义域名访问你的 API。

通过以上步骤,你就可以成功搭建一个使用 Cloudflare Workers 获取必应每日美图的 API。


上面的代码我替换了国内访问速度较快的必应地址。

然后又想增加通过参数来获取不同分辨率的图片,于是修改了一下代码:

初步优化的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
// 定义支持的分辨率映射 (height -> width)
const supportedResolutions = {
'1080': '1920',
'720': '1280',
'768': '1024',
'600': '800',
'480': '640'
};

// 解析请求中的查询参数
const url = new URL(request.url);
const h = url.searchParams.get('h'); // 获取'h'参数

// 如果没有提供'h'参数或提供的值不在支持的列表中,则使用默认高度
const defaultResolution = '1080';
const height = h && supportedResolutions[h] ? h : defaultResolution;
const width = supportedResolutions[height];

// 获取必应每日美图信息
const bingDailyImageDataURL = 'https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN';
const response = await fetch(bingDailyImageDataURL);
const data = await response.json();

// 构建完整的图片URL
const baseUrl = 'https://s.cn.bing.net/';
let imageUrl = `${baseUrl}${data.images[0].url}`;

// 必应提供的原始链接可能是基于1920x1080分辨率的
// 我们需要根据请求的高度调整URL
const defaultResolutionStr = '1920x1080';
const requestedResolutionStr = `${width}x${height}`;

// 替换默认分辨率标识符为请求的高度对应的分辨率
if (imageUrl.includes(defaultResolutionStr)) {
imageUrl = imageUrl.replace(defaultResolutionStr, requestedResolutionStr);
} else {
// 如果URL不包含默认分辨率标识符,尝试通过其他方式获取所需分辨率图片
// 注意:这一步取决于必应的图片链接格式,可能需要根据实际情况调整
const imageBase = imageUrl.split('_').slice(0, -1).join('_'); // 假设图片链接是按某种模式命名的
imageUrl = `${imageBase}_${requestedResolutionStr}.jpg`;
}

// 返回302重定向响应到指定高度的图片
return Response.redirect(imageUrl, 302);
}

这个代码首先定义了一个对象,用于存储支持的分辨率映射。然后,它从请求的查询参数中获取’h’参数,并检查它是否存在于支持的分辨率映射中。如果不存在,则使用默认的分辨率。

接着优化:每天第一次访问时,会请求必应的API,获取今日美图的信息,然后根据请求的分辨率参数,构建完整的图片URL,然后把构建好的URL作为重定向目标返回给客户端,同时缓存到KV中,下次再访问时,直接从KV中读取图片URL,减少对必应的请求次数。但是这样一来,跟必应的API更新时间有一点差距,但无伤大雅。

优化完成的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});

// 直接指定KV命名空间
const kvNamespace = globalThis['bing.worker'];

// 定义支持的分辨率映射 (height -> width),作为全局变量
const supportedResolutions = {
'1080': '1920',
'720': '1280',
'768': '1024',
'600': '800',
'480': '640',
'2160': '3840', // UHD: 3840x2160
'1920': '1080', // FHD: 1080x1920
'1280': '720' // HD: 720x1280
};

// 特殊分辨率标识符映射
const specialResolutionIdentifiers = {
'3840x2160': 'UHD'
};

async function handleRequest(request) {
try {
const url = new URL(request.url);
const h = url.searchParams.get('h') || '1080'; // 默认高度
const requestedResolutionStr = `${supportedResolutions[h] || '1920'}x${h}`;

// 首先尝试从KV中获取指定分辨率的图片链接
const cachedImageUrl = await kvNamespace.get(requestedResolutionStr, 'text');

if (cachedImageUrl) {
// 如果缓存中存在,则直接返回
return Response.redirect(cachedImageUrl, 302);
}

// 如果缓存中不存在,则从必应API获取图片链接并构建所有支持分辨率的图片链接
const imageLinks = await fetchAndCacheBingDailyImage();

// 构建并返回请求的图片链接
const imageUrl = imageLinks[requestedResolutionStr] || imageLinks['1920x1080']; // 默认回退到1920x1080

// 返回新的图片链接给客户端
return Response.redirect(imageUrl, 302);
} catch (error) {
console.error('Error handling request:', error);
return new Response('Internal Server Error', { status: 500 });
}
}

async function fetchAndCacheBingDailyImage() {
try {
// 使用原始必应API获取每日美图数据
const bingDailyImageDataURL = 'https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN';
const response = await fetch(bingDailyImageDataURL);

if (!response.ok) {
throw new Error(`Failed to fetch Bing daily image: ${response.statusText}`);
}

const data = await response.json();
const baseUrl = 'https://cn.bing.com';
let originalImageUrl = `${baseUrl}${data.images[0].url}`;

// 清理URL,移除不必要的参数
const cleanedImageUrl = cleanImageUrl(originalImageUrl);

// 构建所有支持分辨率的图片链接
const imageLinks = Object.entries(supportedResolutions).reduce((acc, [height, width]) => {
const resolutionStr = `${width}x${height}`;
// 检查是否是特殊分辨率,并使用对应的标识符替换默认分辨率字符串
const replacement = specialResolutionIdentifiers[resolutionStr] || resolutionStr;
acc[resolutionStr] = cleanedImageUrl.replace('1920x1080', replacement);
return acc;
}, {});

// 将所有支持分辨率的图片链接存储到KV中,设置8小时的过期时间
await Promise.all(
Object.keys(imageLinks).map(resolution =>
kvNamespace.put(resolution, imageLinks[resolution], { expirationTtl: 8 * 60 * 60 })
)
);

return imageLinks;
} catch (error) {
console.error('Error fetching and caching Bing daily image:', error);
throw error;
}
}

function cleanImageUrl(url) {
try {
// 解析URL并移除特定的查询参数
const urlObj = new URL(url);
// 移除不需要的查询参数(如rf和pid)
urlObj.searchParams.delete('rf');
urlObj.searchParams.delete('pid');

// 返回清理后的URL字符串
return urlObj.toString();
} catch (error) {
console.error('Error cleaning URL:', error);
throw error;
}
}

使用/?h=720请求这个API,就可以得到一个302重定向到1280x720分辨率的图片。

效果如下:

必应每日美图
必应每日美图

API 已部署在 Cloudflare 上,可以通过访问这个链接来获取不同分辨率的图片。如果不附带参数或请求的参数不在支持的列表中,则返回默认的分辨率(1920x1080)。

参数对应分辨率
21603840x2160
19201080x1920
1280720x1280
10801920x1080
7201280x720
7681024x768
600800x600
480640x480

PS:workers的默认域名workers.dev在国内无法直接访问,需要设置自定义域名后才能访问。


2026年回顾:时隔两年的二次优化

距离写这篇文章已经过去一年半了。最近在整理 Cloudflare 账户时重新审视了这个 Worker,发现虽然它一直在稳定跑着,但代码里藏着几个小毛病,干脆趁这个机会给它做了一次大保健。

发现了什么?横屏 vs 竖屏

先说一下必应壁纸的一个特点:同一张图有横屏竖屏两个版本:

1
2
3
4
# 横屏版(桌面壁纸)
https://cn.bing.com/...SichuanTea_1920x1080.jpg → 200 ✓
# 竖屏版(手机壁纸)
https://cn.bing.com/...SichuanTea_1080x1920.jpg → 200 ✓

必应的 CDN 会为每张图生成不同裁切方向的版本,横屏给桌面用,竖屏给手机用。

所以代码里原有的 '1920': '1080''1280': '720' 不是 bug,而是竖屏模式的映射。只不过原来的命名把横竖混在一起,看起来有点乱。

做了哪些改进

我把横屏和竖屏分成了两组,加上注释说明:

优化项 原来的问题 改了什么
🗂️ 分辨率分组 横竖混在一起看不明白 LANDSCAPE / PORTRAIT 分开定义,再合并
💬 参数提示 传错只返回支持列表 多加一句提示:>=1080 是竖屏,<1080 是横屏
🐛 4K 标识符 原来的特殊映射逻辑不够直观 统一处理:3840x2160UHD

现在的参数说明

参数 分辨率 方向 适用场景
?h=2160 3840×2160 横屏 🖥️ 4K 桌面壁纸
?h=1080 1920×1080 横屏 🖥️ Full HD 桌面壁纸
?h=720 1280×720 横屏 🖥️ HD 桌面壁纸
?h=768 1024×768 横屏 🖥️ XGA
?h=600 800×600 横屏 🖥️ SVGA
?h=480 640×480 横屏 🖥️ VGA
?h=1920 1080×1920 竖屏 📱 手机壁纸 FHD
?h=1280 720×1280 竖屏 📱 手机壁纸 HD

改了哪些地方

优化项 原来的问题 改了什么
🐛 分辨率映射 最后三条是竖屏/重复映射 去掉有问题的条目,只保留标准横屏分辨率
🛡️ 参数校验 ?h=999 静默回退到 1080 传错参数返回 400 + 提示支持的分辨率列表
🔄 容错降级 Bing API 挂了直接 500 自动用 KV 里的旧缓存降级,实在没有才 503
⚡ 缓存优化 302 重定向没有 Cache-Control 加了 max-age=21600,浏览器/CDN 都能缓存
📦 写入优化 逐个分辨率串行写入 KV Promise.all 并行写入,快了一点点
📝 代码结构 注释较少,逻辑混在一起 分区清晰,每段功能都有注释说明

优化后的效果

1
2
3
4
5
6
7
8
9
# ✅ 正常请求 → 302 重定向到壁纸
$ curl -I https://bing.worker.huanzhi.wang/?h=1080
HTTP/2 302
location: https://cn.bing.com/th?id=OHR.SichuanTea_ZH-CN...1920x1080.jpg
cache-control: public, max-age=21600

# ✅ 传错参数 → 400 友好提示
$ curl https://bing.worker.huanzhi.wang/?h=999
{"error":"Unsupported height. Supported: 480, 600, 720, 768, 1080, 2160"}

最终完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// ==============================================
// Bing Daily Wallpaper Proxy Worker (v4)
// 支持横屏 & 竖屏(手机壁纸)分辨率
// ==============================================

const BING_KV = globalThis['bing.worker'];

// 横屏分辨率: height → width
const LANDSCAPE = {
'2160': '3840', // 4K UHD
'1080': '1920', // Full HD
'720': '1280', // HD
'768': '1024', // XGA
'600': '800', // SVGA
'480': '640', // VGA
};

// 竖屏分辨率: height → width(手机壁纸)
const PORTRAIT = {
'1920': '1080', // FHD 竖屏: 1080x1920
'1280': '720', // HD 竖屏: 720x1280
};

const ALL_RESOLUTIONS = { ...LANDSCAPE, ...PORTRAIT };
const DEFAULT_HEIGHT = '1080';
const KV_CACHE_TTL = 8 * 60 * 60;
const EDGE_CACHE_TTL = 6 * 60 * 60;

addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
try {
const url = new URL(request.url);
const h = url.searchParams.get('h') || DEFAULT_HEIGHT;

if (!ALL_RESOLUTIONS[h]) {
const supported = Object.keys(ALL_RESOLUTIONS).join(', ');
return new Response(
JSON.stringify({
error: `Unsupported height. Supported: ${supported}`,
note: 'Values >= 1080 are portrait; < 1080 are landscape'
}),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}

const width = ALL_RESOLUTIONS[h];
const resolutionKey = `${width}x${h}`;
let imageUrl = await BING_KV.get(resolutionKey, 'text');

if (!imageUrl) {
const allLinks = await fetchAndCacheAllResolutions();
if (!allLinks) {
const fallback = await getAnyCachedImage();
if (fallback) return redirectWithCache(fallback);
return new Response('Service temporarily unavailable', { status: 503 });
}
imageUrl = allLinks[resolutionKey] || allLinks['1920x1080'];
}

return redirectWithCache(imageUrl);
} catch (err) {
console.error('Fatal error:', err);
return new Response('Internal Server Error', { status: 500 });
}
}

function redirectWithCache(imageUrl) {
return new Response(null, {
status: 302,
headers: {
Location: imageUrl,
'Cache-Control': `public, max-age=${EDGE_CACHE_TTL}`,
},
});
}

async function fetchAndCacheAllResolutions() {
try {
const apiUrl = 'https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN';
const resp = await fetch(apiUrl);
if (!resp.ok) return null;

const data = await resp.json();
if (!data?.images?.[0]?.url) return null;

const baseUrl = 'https://cn.bing.com';
let originalUrl = `${baseUrl}${data.images[0].url}`;

try {
const u = new URL(originalUrl);
u.searchParams.delete('rf');
u.searchParams.delete('pid');
originalUrl = u.toString();
} catch (_) {}

const imageLinks = {};
const kvPromises = [];

for (const [height, width] of Object.entries(ALL_RESOLUTIONS)) {
const res = `${width}x${height}`;
const replacement = res === '3840x2160' ? 'UHD' : res;
imageLinks[res] = originalUrl.replace('1920x1080', replacement);
kvPromises.push(
BING_KV.put(res, imageLinks[res], { expirationTtl: KV_CACHE_TTL })
);
}

await Promise.all(kvPromises);
return imageLinks;
} catch (err) {
console.error('fetchAndCacheAllResolutions error:', err);
return null;
}
}

async function getAnyCachedImage() {
try {
const keys = await BING_KV.list({ limit: 1 });
if (keys.keys.length > 0) {
return await BING_KV.get(keys.keys[0].name, 'text');
}
} catch (_) {}
return null;
}

更新后的分辨率表也重新整理一下,横屏竖屏分开:

参数 分辨率 方向 适用场景
2160 3840×2160 横屏 🖥️ 4K 桌面
1080 1920×1080 横屏 🖥️ Full HD
720 1280×720 横屏 🖥️ HD
768 1024×768 横屏 🖥️ XGA
600 800×600 横屏 🖥️ SVGA
480 640×480 横屏 🖥️ VGA
1920 1080×1920 竖屏 📱 手机壁纸 FHD
1280 720×1280 竖屏 📱 手机壁纸 HD

写在最后

这个 Worker 从 2024 年 12 月写完第一版到现在,默默跑了快一年半,中间没出过什么岔子。这次优化更多是”既然发现了就顺手修一下”的心态,也顺便给自己提个醒——代码写完当时觉得没问题,过段时间回来看可能就能发现一些之前没注意到的细节。

必应的每日壁纸质量一直在线,我的博客封面图到现在也还在用这个接口。如果你也在用,可以试试传个不同的 ?h= 参数看看效果。😊