前言

之前写过用自己的服务器检测友情链接状态,但是都是自己服务器访问的,有时候就会出现访问错误的情况

今天教大家如何使用 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);
}

至此大功告成,一键三连即可查看效果