前言
之前写过用自己的服务器检测友情链接状态,但是都是自己服务器访问的,有时候就会出现访问错误的情况
今天教大家如何使用 cloudflare works 来部署检测
效果

教程
注册并登录cloudflare
托管一个域名到 cloudflare (网上一搜就有大把教程)
新建一个 KV 空间,命名为links-status,一定要命名这个,否则会报错
新建一个 works,选择Hello Word模板进行创建,然后编辑代码,将如下代码放到里面
const HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 (check-flink/1.0; +https://绑定的域名/bot)",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
"X-Check-Flink": "1.0"
};
const SOURCE_HEADERS = {
"Accept": "application/json",
"Referer": "https://绑定的域名/",
"Origin": "https://绑定的域名",
"Accept-Language": "zh-CN,zh;q=0.9",
"sec-ch-ua": "\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\", \"Google Chrome\";v=\"122\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"X-Check-Flink": "1.0"
};
const RAW_HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 (check-flink/1.0; +https://link.ityr.xyz/bot)",
"Accept": "application/json",
"X-Check-Flink": "1.0"
};
const SOURCE_URL = '你的json文件';
const CACHE_KEY = 'latest_status';
const FAIL_COUNT_KEY = 'fail_counts';
const CF_STATUS_KEY = 'cloudflare_status';
const XIAOXIAO_STATUS_KEY = 'xiaoxiao_status';
// 辅助函数:格式化为上海时间
function formatShanghaiTime(date) {
const shanghaiDate = new Date(date);
shanghaiDate.setHours(shanghaiDate.getHours() + 8);
const year = shanghaiDate.getFullYear();
const month = String(shanghaiDate.getMonth() + 1).padStart(2, '0');
const day = String(shanghaiDate.getDate()).padStart(2, '0');
const hours = String(shanghaiDate.getHours()).padStart(2, '0');
const minutes = String(shanghaiDate.getMinutes()).padStart(2, '0');
const seconds = String(shanghaiDate.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
async function fetchSourceLinks() {
try {
const response = await fetch(SOURCE_URL, {
headers: SOURCE_HEADERS,
redirect: 'follow'
});
if (!response.ok) {
throw new Error(`获取源数据失败: HTTP ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error(`fetchSourceLinks 错误: ${error.message}`);
throw new Error(`获取源数据失败: ${error.message}`);
}
}
async function checkLinkDirectly(url) {
try {
const startTime = Date.now();
const response = await fetch(url, {
headers: HEADERS,
redirect: 'follow',
cf: {
cacheTtl: 0 // 禁用Cloudflare缓存
}
});
const latency = Math.round((Date.now() - startTime) / 10) / 100;
return {
success: response.status === 200,
latency: response.status === 200 ? latency : -1,
status: response.status
};
} catch (error) {
console.error(`checkLinkDirectly 错误 (${url}): ${error.message}`);
return {
success: false,
latency: -1,
status: 0,
error: error.message
};
}
}
// 添加并发控制函数
async function batchProcess(items, batchSize, processor) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(batch.map(processor));
results.push(...batchResults);
if (i + batchSize < items.length) {
await new Promise(resolve => setTimeout(resolve, 200)); // 批次间延迟
}
}
return results;
}
async function checkWithAPI(items) {
const results = [];
const xiaoxiaoStatus = {};
const batchSize = 10;
const processItem = async (item) => {
const url = item.link;
if (!url) {
return {
...item,
success: false,
latency: -1,
needDirectCheck: true
};
}
try {
const apiUrl = `https://v2.xxapi.cn/api/status?url=${encodeURIComponent(url)}`;
const response = await fetch(apiUrl, {
headers: SOURCE_HEADERS,
timeout: 30000,
cf: {
cacheTtl: 0 // 禁用Cloudflare缓存
}
});
if (response.ok) {
const data = await response.json();
const statusCode = parseInt(data.data);
const success = parseInt(data.code) === 200 && (statusCode >= 200 && statusCode < 400);
xiaoxiaoStatus[url] = {
success,
status: statusCode,
apiStatus: parseInt(data.code),
latency: success ? (data.latency || 0) : -1,
timestamp: formatShanghaiTime(new Date())
};
// 如果API返回的状态码不是2xx或3xx,需要直接检查
const needDirectCheck = !success;
return {
...item,
success,
latency: success ? (data.latency || 0) : -1,
needDirectCheck
};
} else {
xiaoxiaoStatus[url] = {
success: false,
status: 0,
apiStatus: response.status,
latency: -1,
timestamp: formatShanghaiTime(new Date())
};
return {
...item,
success: false,
latency: -1,
needDirectCheck: true
};
}
} catch (error) {
console.error(`checkWithAPI 错误 (${url}): ${error.message}`);
xiaoxiaoStatus[url] = {
success: false,
status: 0,
apiStatus: 0,
latency: -1,
error: error.message,
timestamp: formatShanghaiTime(new Date())
};
return {
...item,
success: false,
latency: -1,
needDirectCheck: true
};
}
};
const processedResults = await batchProcess(items, batchSize, processItem);
results.push(...processedResults);
return { results, xiaoxiaoStatus };
}
async function checkAllLinks(env) {
try {
const sourceData = await fetchSourceLinks();
if (!sourceData || !sourceData.link_list || !Array.isArray(sourceData.link_list)) {
throw new Error('源数据格式错误');
}
let failCounts = {};
try {
const storedFailCounts = await env["links-status"].get(FAIL_COUNT_KEY);
if (storedFailCounts) {
failCounts = JSON.parse(storedFailCounts);
}
} catch (error) {
console.error(`获取失败计数错误: ${error.message}`);
failCounts = {};
}
const linksToCheck = sourceData.link_list;
const cfStatus = {};
// 先用小小API检查所有链接
const { results: apiResults, xiaoxiaoStatus } = await checkWithAPI(linksToCheck);
// 找出需要直接检查的链接
const needDirectCheck = apiResults.filter(item => item.needDirectCheck);
// 直接检查需要检查的链接
const batchSize = 10;
const processDirectCheck = async (item) => {
const result = await checkLinkDirectly(item.link);
cfStatus[item.link] = {
success: result.success,
status: result.status,
latency: result.latency,
error: result.error,
timestamp: formatShanghaiTime(new Date())
};
return {
name: item.name,
link: item.link,
latency: result.latency,
success: result.success,
fail_count: failCounts[item.link] || 0
};
};
const directResults = needDirectCheck.length > 0
? await batchProcess(needDirectCheck, batchSize, processDirectCheck)
: [];
// 合并结果
const finalResults = apiResults.map(item => {
if (!item.needDirectCheck) {
// 使用小小API的结果
return {
name: item.name,
link: item.link,
latency: item.latency,
fail_count: item.success ? 0 : (failCounts[item.link] || 0) + 1
};
} else {
// 使用直接检查的结果
const directResult = directResults.find(r => r.link === item.link);
if (directResult) {
// 优先使用直接检查的结果
return {
name: item.name,
link: item.link,
latency: directResult.latency,
fail_count: directResult.success ? 0 : (failCounts[item.link] || 0) + 1
};
}
// 如果都失败了
return {
name: item.name,
link: item.link,
latency: -1,
fail_count: (failCounts[item.link] || 0) + 1
};
}
});
// 更新失败计数
finalResults.forEach(item => {
if (item.fail_count > 0) {
failCounts[item.link] = item.fail_count;
} else {
delete failCounts[item.link];
}
});
const now = new Date();
await Promise.all([
env["links-status"].put(FAIL_COUNT_KEY, JSON.stringify(failCounts)),
env["links-status"].put(CF_STATUS_KEY, JSON.stringify(cfStatus)),
env["links-status"].put(XIAOXIAO_STATUS_KEY, JSON.stringify(xiaoxiaoStatus))
]);
const accessible = finalResults.filter(r => r.fail_count === 0).length;
const resultData = {
timestamp: formatShanghaiTime(now),
accessible_count: accessible,
inaccessible_count: finalResults.length - accessible,
total_count: finalResults.length,
link_status: finalResults
};
return resultData;
} catch (error) {
console.error(`checkAllLinks 错误: ${error.message}`);
throw error;
}
}
async function handleScheduled(env) {
try {
const result = await checkAllLinks(env);
await env["links-status"].put(CACHE_KEY, JSON.stringify(result));
return result;
} catch (error) {
console.error(`定时任务错误: ${error.message}`);
return { error: `定时任务失败: ${error.message}` };
}
}
async function handleRequest(request, env) {
try {
const url = new URL(request.url);
if (url.pathname === '/') {
const responseData = {
code: 200,
msg: "获取成功",
data: {
"/status": "检测返回", // 监控状态
"/status/cf": "CF检测返回", // CF返回状态
"/status/xiaoxiao": "API检测返回", // 小小API返回状态
"/health": "运行状态" // 运行状态
}
};
return new Response(
JSON.stringify(responseData),
{
status: 200,
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache"
}
}
);
}
if (url.pathname === '/health') {
return new Response('OK', { status: 200 });
}
if (url.pathname === '/status') {
const cachedResult = await env["links-status"].get(CACHE_KEY);
if (!cachedResult) {
const result = await handleScheduled(env);
if (result.error) {
throw new Error(result.error);
}
return new Response(JSON.stringify(result), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
}
});
}
return new Response(cachedResult, {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
}
});
}
if (url.pathname === '/status/cf') {
const cfStatus = await env["links-status"].get(CF_STATUS_KEY);
return new Response(cfStatus || '{}', {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
}
});
}
if (url.pathname === '/status/xiaoxiao') {
const xiaoxiaoStatus = await env["links-status"].get(XIAOXIAO_STATUS_KEY);
return new Response(xiaoxiaoStatus || '{}', {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
}
});
}
return new Response('Not Found', { status: 404 });
} catch (error) {
console.error(`handleRequest 错误: ${error.message}`);
return new Response(JSON.stringify({
error: '处理请求失败',
detail: error.message
}), {
status: 500,
headers: {
'Content-Type': 'application/json'
}
});
}
}
export default {
async fetch(request, env, ctx) {
return handleRequest(request, env);
},
async scheduled(event, env, ctx) {
return handleScheduled(env);
}
};
然后就是json文件的生成了,在scripts文件夹中新建一个js文件,将如下代码放进去
const fs = require('fs');
const path = require('path');
hexo.extend.generator.register('links-api', function(locals) {
const linksData = hexo.locals.get('data').links;
if (!linksData) {
return;
}
// 获取实际的链接数据
const actualLinksData = linksData.links;
if (!actualLinksData || !Array.isArray(actualLinksData)) {
return;
}
// 查找"小伙伴"分类
let xiaohuobanCategory = null;
for (const category of actualLinksData) {
if (category.class_name === '小伙伴') {
xiaohuobanCategory = category;
break;
}
}
if (!xiaohuobanCategory || !xiaohuobanCategory.link_list) {
return;
}
// 构建API数据
const apiData = {
link_list: xiaohuobanCategory.link_list,
length: xiaohuobanCategory.link_list.length,
updated_time: new Date().toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).replace(/\//g, '-'),
timezone: 'Asia/Shanghai'
};
// 确保目录存在
const apiDir = path.join(hexo.public_dir, 'api');
if (!fs.existsSync(apiDir)) {
fs.mkdirSync(apiDir, { recursive: true });
}
// 写入JSON文件
const filePath = path.join(apiDir, 'links.json');
fs.writeFileSync(filePath, JSON.stringify(apiData, null, 2), 'utf8');
return {
path: 'api/links.json',
data: JSON.stringify(apiData, null, 2)
};
});
这样构建的时候就会生成一个json文件了,最后就是在友链页面显示了。
在source文件夹中的js文件夹内新建一个js文件,并且将如下代码放进去
// 友链状态检测脚本
let retryCount = 0;
const MAX_RETRIES = 3;
const STATUS_URL = 'https://绑定的域名/status';
function fetchLinkStatus() {
return fetch(STATUS_URL)
.then(response => {
if (!response.ok) {
throw new Error('网络请求失败');
}
return response.json();
});
}
function updateLinkStatus(data) {
if (data && data.link_status) {
const statusMap = {};
data.link_status.forEach(link => {
statusMap[link.name] = link;
});
// 更新每个链接的状态 - 只处理小伙伴分类中的状态
const statusElements = document.querySelectorAll('.site-card-status');
statusElements.forEach(el => {
const linkName = el.getAttribute('data-name');
if (statusMap[linkName]) {
const status = statusMap[linkName];
let statusClass = '';
let statusText = '';
if (status.latency === -1) {
statusClass = 'status-error';
const errorCount = status.fail_count || 0;
statusText = "异常[" + errorCount + "]";
} else if (status.latency > 4) {
statusClass = 'status-slow';
statusText = '缓慢';
} else {
statusClass = 'status-normal';
statusText = '正常';
}
// 更新状态
el.className = 'site-card-status ' + statusClass;
el.textContent = statusText;
el.classList.remove('status-loading');
}
});
} else {
throw new Error('无效的状态数据');
}
}
function handleError() {
retryCount++;
if (retryCount <= MAX_RETRIES) {
setTimeout(fetchAndUpdateStatus, 2000 * retryCount); // 2秒、4秒、6秒后重试
} else {
// 将所有状态设为错误
document.querySelectorAll('.site-card-status.status-loading').forEach(el => {
el.className = 'site-card-status status-error';
el.textContent = '获取失败';
});
}
}
function fetchAndUpdateStatus() {
fetchLinkStatus()
.then(data => updateLinkStatus(data))
.catch(error => {
console.error('获取友链状态失败:', error);
handleError();
});
}
// 页面加载后立即执行
document.addEventListener('DOMContentLoaded', fetchAndUpdateStatus);
// 支持PJAX重新加载
document.addEventListener('pjax:complete', fetchAndUpdateStatus);
因为我只有小伙伴分类,所以只弄了小伙伴,如果你有其他分类可以将js代码丢给AI让其处理添加其他分类。
接着继续,在_config.solitude.yml文件中找到extends配置,并且在body中添加如下代码
- <script src="/js/links-status.js"></script>
然后就是CSS样式了,在你的自定义CSS文件中添加如下代码
/* 友链状态标签样式 */
.site-card {
position: relative;
}
.flink-list-item {
position: relative;
}
.site-card-status {
position: absolute;
bottom: 0;
right: 0;
z-index: 10;
padding: 3px 8px;
border-radius: 8px 0 8px 0;
font-size: 12px;
font-weight: 500;
color: #fff;
background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
transition: all 0.3s ease;
}
.site-card-status.status-loading {
background-color: rgba(100, 100, 100, 0.8);
animation: pulse 1.5s infinite;
}
.site-card-status.status-normal {
background-color: rgba(82, 196, 26, 0.9);
}
.site-card-status.status-slow {
background-color: rgba(250, 173, 20, 0.9);
}
.site-card-status.status-error {
background-color: rgba(255, 77, 79, 0.9);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* 深色模式适配 */
[data-theme="dark"] .site-card-status {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
}
[data-theme="dark"] .site-card-status.status-loading {
background-color: rgba(100, 100, 100, 0.8);
}
[data-theme="dark"] .site-card-status.status-normal {
background-color: rgba(82, 196, 26, 0.8);
}
[data-theme="dark"] .site-card-status.status-slow {
background-color: rgba(250, 173, 20, 0.8);
}
[data-theme="dark"] .site-card-status.status-error {
background-color: rgba(255, 77, 79, 0.8);
}
至此大功告成,一键三连即可查看效果