/**------【①.谋而后定:配置区】-----**/ 'use strict'; // 我们将在请求处理函数中加载环境变量 const ACCOUNT = { //账号相关,安全性更高 "user" : "", // 将在请求处理时动态获取 "password" : "", // 将在请求处理时动态获取 "third_token" : "", // 将在请求处理时动态获取 "cacheZoneId": "", // 将在请求处理时动态获取 "cacheToken": "", // 将在请求处理时动态获取 "kv_var": this['CFBLOG'],//workers绑定kv时用的变量名 } const OPT = { //网站配置 /*--前台参数--*/ "siteDomain" : "blog.example.com",// 域名(不带https 也不带/) "siteName" : "CFBLOG-Plus",//博客名称 "siteDescription":"CFBLOG-Plus" ,//博客描述 "keyWords":"cloudflare,KV,workers,blog",//关键字 "logo":"https://cdn.jsdelivr.net/gh/Arronlong/cfblog-plus@master/themes/JustNews/files/logo2.png",//JustNews主题的logo "theme_github_path":"https://cdn.jsdelivr.net/gh/Arronlong/cfblog-plus@master/themes/",//主题路径 "themeURL" : "https://raw.githubusercontent.com/Arronlong/cfblog-plus/master/themes/JustNews/", // 模板地址,以 "/"" 结尾 //"search_xml_url":"", //search.xml外部链接,可通过github的action自动生成,不设置则实时生成 //"sitemap_xml_url":"", //sitemap.xml外部链接,可通过github的action自动生成,不设置则实时生成 "pageSize" : 5,//每页文章数 "recentlySize" : 6,//最近文章数 "recentlyType" : 1,//最近文章类型:1-按创建时间倒序(按id倒序),2-按修改时间排序 "readMoreLength":150,//阅读更多截取长度 "cacheTime" : 60*60*24*2, //文章在浏览器的缓存时长(秒),建议=文章更新频率 "html404" : `404`,//404页面代码 "codeBeforHead":` `,//其他代码,显示在前 "codeBeforBody":` `,//其他代码,显示在前 "commentCode":` `,//评论区代码 "widgetOther":` `,//20201224新增参数,用于右侧 小部件扩展 "otherCodeA":`热度`,//模板开发用的其他自定义变量 "otherCodeB":``,// "otherCodeC":``,// "otherCodeD":``,// "otherCodeE":``,// "copyRight" :`Powered by Cloudflare`,//自定义版权信息,建议保留大公无私的 Coudflare 和 作者 的链接 "robots":`User-agent: * Disallow: /admin`,//robots.txt设置 /*--前后台共用参数--*/ "top_flag":`[置顶]`,//置顶标志 "top_flag_style":``,//置顶标志的样式 /*--后台参数--*/ "hidden_flag":`[隐藏]`,//隐藏标志 "hidden_flag_style":``,//隐藏标志的样式 "admin_home_idx": 1, //后台首页tab索引设置:1-我的文章,2-新建,3-设置,4-发布 "editor_page_scripts": ` //置顶设置 let top_setting=\`
\` $('form#addNewForm div.form-group,form#editForm div.form-group').last().after(top_setting);//新建和编辑页面添加置顶设置 $("#istop").change(function(){ $("#top_timestamp").val($(this).val()*1?new Date().getTime():0); }); if(location.pathname.startsWith('/admin/edit')){//修改文章页面,自动设置置顶 $("#istop").val(articleJson.top_timestamp?1:0); $("#top_timestamp").val(articleJson.top_timestamp?articleJson.top_timestamp:0); } $("#istop").trigger('change') //隐藏设置 let hidden_setting=\`
\` $('form#addNewForm div.form-group,form#editForm div.form-group').last().after(hidden_setting);//新建和编辑页面添加隐藏设置 if(location.pathname.startsWith('/admin/edit')){//修改文章页面,自动设置隐藏 $("#hidden").val(articleJson.hidden?1:0); // 只在编辑页面添加删除按钮 let delete_btn = $(''); $('.btn-primary').after(delete_btn); // 添加删除功能 delete_btn.click(function(e) { e.preventDefault(); if(confirm('确定要删除这篇文章吗?此操作不可恢复!')) { $(this).prop('disabled', true).text('删除中...'); fetch('/admin/delete/' + articleJson.id, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' } }) .then(response => response.json()) .then(data => { if(data.status === 'success') { alert('文章删除成功!'); // 修改这里:跳转到文章列表标签页 window.location.replace('/admin/#list'); } else { alert('删除失败:' + (data.message || '未知错误')); $(this).prop('disabled', false).text('删除文章'); } }) .catch(error => { console.error('删除文章时出错:', error); alert('删除文章时出错,请查看控制台'); $(this).prop('disabled', false).text('删除文章'); }); } }); } let sitemapxml=\`导出sitemap.xml\` $('form#importForm a').last().after(sitemapxml);//设置页面添加导出sitemap.xml导出按钮 let searchxml=\`导出search.xml\` $('form#importForm a').last().after(searchxml);//设置页面添加导出search.xml导出按钮 //关闭email匹配和@匹配,否则图片使用jsdelivr的cdn,如果有版本号会匹配成"mailto:xxx"从而导致显示异常 mdEditor.settings.emailLink=false; mdEditor.settings.atLink=false; //默认图片,工具:https://tool.lu/imageholder/ if($('#img').val()=="")$('#img').val('https://cdn.jsdelivr.net/gh/Arronlong/cdn@master/cfblog/cfblog-plus.png'); //默认时间设置为当前时间 if($('#createDate').val()=="")$('#createDate').val(new Date(new Date().getTime()+8*60*60*1000).toJSON().substr(0,16)); // 添加导入功能的处理代码 $(document).ready(function() { // 导入按钮点击处理 $('#btn_import').click(function(e) { e.preventDefault(); let importJson = $('#importJson').val().trim(); if(!importJson) { alert('请输入要导入的JSON数据'); return; } // 显示导入中状态 let $btn = $(this); $btn.prop('disabled', true).text('导入中...'); // 发送导入请求 fetch('/admin/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify([{ name: 'importJson', value: importJson }]) }) .then(response => response.json()) .then(data => { if(data.status === 'success') { alert(data.message); // 导入成功后刷新页面 window.location.reload(); } else { alert('导入失败:' + (data.message || '未知错误')); } }) .catch(error => { console.error('导入出错:', error); alert('导入出错,请查看控制台'); }) .finally(() => { // 恢复按钮状态 $btn.prop('disabled', false).text('导入'); }); }); }); `, //后台编辑页面脚本 }; //---对部分配置进行处理--- { //CFBLOG 通用变量 this.CFBLOG = ACCOUNT.kv_var; //默认为非私密博客 if(null==OPT.privateBlog){ OPT.privateBlog=false; } //处理themeURL、theme_github_path参数设定 if(OPT.themeURL.substr(-1)!='/'){ OPT.themeURL=OPT.themeURL+'/'; } if(OPT.theme_github_path.substr(-1)!='/'){ OPT.theme_github_path=OPT.theme_github_path+'/'; } //置顶样式对于前台来说,与codeBeforHead结合即可 if(OPT.top_flag_style){ OPT.codeBeforHead += OPT.top_flag_style } } /**------【②.猎杀时刻:请求处理入口】-----**/ //监听请求 addEventListener("fetch", event => { // 从环境变量加载敏感配置 loadEnvVariables(); // 处理请求 event.respondWith(handlerRequest(event)); }); // 从环境变量加载配置 function loadEnvVariables() { try { // 在Cloudflare Workers中,环境变量可以直接访问 ACCOUNT.user = BLOG_USER || ""; ACCOUNT.password = BLOG_PASSWORD || ""; ACCOUNT.third_token = BLOG_THIRD_TOKEN || ""; ACCOUNT.cacheZoneId = BLOG_CACHE_ZONE_ID || ""; ACCOUNT.cacheToken = BLOG_CACHE_TOKEN || ""; console.log("环境变量加载成功"); } catch (e) { console.error("环境变量加载失败", e); } } // 处理请求 async function handlerRequest(event){ let request = event.request let url = new URL(request.url) let paths = url.pathname.trim("/").split("/") // 处理登录页面请求 if (paths[0] === "login") { return await handle_login(request); } // 修改校验权限的逻辑 if (("admin" == paths[0] || true === OPT.privateBlog)) { // 检查cookie中的auth令牌 const cookies = request.headers.get('Cookie') || ''; let authToken = ''; cookies.split(';').forEach(cookie => { const [name, value] = cookie.trim().split('='); if (name === 'auth') { authToken = value; } }); // 如果有auth令牌,添加到请求头 if (authToken) { const newHeaders = new Headers(request.headers); newHeaders.set('Authorization', 'Basic ' + authToken); request = new Request(request.url, { method: request.method, headers: newHeaders, body: request.body }); } // 如果认证失败,重定向到登录页 if (!parseBasicAuth(request)) { return new Response("Redirecting to login...", { status: 302, headers: { "Location": "/login" } }); } } // 尝试从缓存获取,仅对GET请求 if (request.method === 'GET') { // 构建缓存键 const cacheKey = "https://" + OPT.siteDomain + url.pathname; const cache = caches.default; // 尝试从缓存获取 let response = await cache.match(new Request(cacheKey, request)); if (response) { console.log("命中缓存:", cacheKey); return response; } } // 处理实际请求 let response; try { switch(paths[0]){ case "favicon.ico": //图标 response = await handle_favicon(request); break; case "robots.txt": response = await handle_robots(request); break; case "sitemap.xml": response = await handle_sitemap(request); break; case "search.xml": response = await handle_search(request); break; case "admin": //后台 response = await handle_admin(request); break; case "article": //文章内容页 response = await handle_article(paths[1]); break; case "": //文章 首页 case "page": //文章 分页 case "category": //分类 分页 case "tags": //标签 分页 response = await renderBlog(url); break; default: //其他页面返回404 response = new Response(OPT.html404, { headers: { "content-type": "text/html;charset=UTF-8" }, status: 200 }); break; } } catch (error) { // 错误处理 console.error("请求处理错误:", error); response = new Response("内部服务器错误: " + error.message, { status: 500, headers: { "Content-Type": "text/plain" } }); } // 设置缓存 if (request.method === 'GET' && response.ok && paths[0] !== 'admin') { try { // 克隆响应对象 const cacheResponse = new Response(response.clone().body, response); // 设置缓存控制 cacheResponse.headers.set('Cache-Control', `public, max-age=${OPT.cacheTime}`); // 缓存响应 const cacheKey = "https://" + OPT.siteDomain + url.pathname; event.waitUntil(caches.default.put(new Request(cacheKey, request), cacheResponse)); } catch (e) { console.error("缓存设置错误:", e); } } else if (paths[0] === 'admin') { // 管理页面不缓存 response.headers.set('Cache-Control', 'no-store'); } return response; } /**------【③.分而治之:各种请求分开处理】-----**/ //访问: favicon.ico async function handle_favicon(request){ /* 想要自定义,或者用指定的ico,可将此请求置为404,并在codeBeforHead中自行添加类似代码: */ /* return new Response("404",{ headers:{ "content-type":"text/plain;charset=UTF-8" }, status:404 }); */ let url = new URL(request.url) url.host="dash.cloudflare.com" return await fetch(new Request(url, request)); } //访问: robots.txt async function handle_robots(request){ return new Response(OPT.robots+"\nSitemap: https://"+OPT.siteDomain+"/sitemap.xml",{ headers:{ "content-type":"text/plain;charset=UTF-8" }, status:200 }); } //访问: sitemap.xml async function handle_sitemap(request){ //如果设置了参数,则使用参数指定的url //可使用github action方式自动定期更新 let xml; if(OPT.sitemap_xml_url){ //cf代理方式,速度可以,实时性更好 let url = new URL(request.url) url.href = OPT.sitemap_xml_url.replace('cdn.jsdelivr.net/gh','raw.githubusercontent.com').replace('@','/'); xml = await fetch(new Request(url, request)); xml = await xml.text(); ////302方式,如果使用jsdelivr作为cdn,速度快,但更新有延迟 //return new Response("",{ // headers:{ // "location":OPT.sitemap_xml_url // }, // status:302 //}); }else{ //未配置参数,则实时获取结构 //读取文章列表,并按照特定的xml格式进行组装 let articles_all=await getArticlesList() xml='\n'; for(var i=0;i", xml+="\n\t\t"+articles_all[i].createDate.substr(0,10)+"", xml+="\n\t\t"+(void 0===articles_all[i].changefreq?"daily":articles_all[i].changefreq)+"", xml+="\n\t\t"+(void 0===articles_all[i].priority?"0.5":articles_all[i].priority)+"", xml+="\n\t"; } xml+="\n" } return new Response(xml,{ headers:{ "content-type":"text/xml;charset=UTF-8" }, status:200 }); } //访问: search.xml async function handle_search(request){ //如果设置了参数,则使用参数指定的url //可使用github action方式自动定期更新 let xml; if(OPT.search_xml_url){ //cf代理方式,速度可以,实时性更好 let url = new URL(request.url) url.href = OPT.search_xml_url.replace('cdn.jsdelivr.net/gh','raw.githubusercontent.com').replace('@','/'); xml = await fetch(new Request(url, request)); xml = await xml.text(); ////302方式,如果使用jsdelivr作为cdn,速度快,但更新有延迟 //return new Response("",{ // headers:{ // "location":OPT.search_xml_url // }, // status:302 //}); }else{ //未配置参数,则实时获取结构 //读取文章列表,并按照特定的xml格式进行组装 let articles_all=await getArticlesList() xml='\n'; for(var i=0;i"; let article = await getArticle(articles_all[i].id); if(null != article){ xml+="\n\t\t"+article.contentMD.replaceAll('<','<').replaceAll('>','>').replaceAll('&','&')+"" } xml+="\n\t\thttps://"+OPT.siteDomain+"/article/"+articles_all[i].id+"/"+articles_all[i].link+".html", xml+="\n\t\t", xml+="\n\t"; } xml+="\n" } return new Response(xml,{ headers:{ "content-type":"text/xml;charset=UTF-8" }, status:200 }); } //渲染前端博客:指定一级路径page\tags\category,二级路径value,以及页码,默认第一页 async function renderBlog(url){ console.log("---进入renderBlog函数---,path=", url.href.substr(url.origin.length)) //处理主题预览及分页 let theme=url.searchParams.get("theme"), pageSize=url.searchParams.get("pageSize"); if(theme){ OPT.themeURL=OPT.theme_github_path+theme+"/"; } if(pageSize){ OPT.pageSize=parseInt(pageSize); } //如果采用默认default主题,则改为加载default2.0主题 if(OPT.theme_github_path+"default/"==OPT.themeURL){ OPT.themeURL=OPT.theme_github_path+"default2.0/"; } console.log("theme pageSize",OPT.pageSize,OPT.themeURL) //获取主页模板源码 let theme_html=await getThemeHtml("index"), //KV中读取导航栏、分类目录、标签、链接、所有文章、近期文章等配置信息 menus=await getWidgetMenu(), categories=await getWidgetCategory(), tags=await getWidgetTags(), links=await getWidgetLink(), articles_all=await getArticlesList(), articles_recently=await getRecentlyArticles(articles_all); /** 前台博客 * 路径格式: * 域名/ 文章列表首页,等价于域名/page/1 * 域名/page/xxx 文章列表翻页 * * 域名/category/xxx 分类页,等价于域名/category/xxx/page/1 * 域名/category/xxx/page/xxx 分类页+翻页 * * 域名/tags/xxx 标签页,等价于域名/tags/xxx/page/1 * 域名/tags/xxx/page/xxx 分类页+翻页 * */ let paths = url.pathname.trim("/").split("/") let articles=[], pageNo=1 //获取文章列表 switch(paths[0]||"page"){ case "page": articles = articles_all pageNo = paths[1]||1 break; case "tags": case "category": let category_tag = paths.slice(1).join("");//如果无分页,tags、category后面都是标签、分类名 if(paths.length>3 && paths.includes("page")){ pageNo = paths[paths.indexOf("page")+1] //分页的页码 category_tag = paths.slice(1, paths.lastIndexOf("page")-1).join("") //tags、category后,分页前的为标签、分类名 } category_tag = decodeURIComponent(category_tag) articles = articles_all.filter(a => a[paths[0]].includes(category_tag)) break; } pageNo = parseInt(pageNo) //获取当页要显示文章列表 let articles_show = articles.slice((pageNo-1)*OPT.pageSize,pageNo*OPT.pageSize); //处理文章属性(年月日、url等) processArticleProp(articles_show); let url_prefix = url.pathname.replace(/(.*)\/page\/\d+/,'$1/') if(url_prefix.substr(-1)=='/'){ url_prefix=url_prefix.substr(0,url_prefix.length-1); } //组装各种参数 let newer=[{title:"上一页",url:url_prefix+"/page/"+(pageNo-1)}]; if(1==pageNo){ newer=[]; } let older=[{title:"下一页",url:url_prefix+"/page/"+(pageNo+1)}]; if(pageNo*OPT.pageSize>=articles.length){ older=[]; } //文章标题、关键字 let title=(pageNo>1 ? "page "+pageNo+" - " : "")+OPT.siteName, keyWord=OPT.keyWords, cfg={}; cfg.widgetMenuList=menus,//导航 cfg.widgetCategoryList=categories,//分类目录 cfg.widgetTagsList=tags,//标签 cfg.widgetLinkList=links,//链接 cfg.widgetRecentlyList=articles_recently,//近期文章 cfg.articleList=articles_show,//当前页文章列表 cfg.pageNewer=newer,//上翻页链接 cfg.pageOlder=older,//下翻页链接 cfg.title=title,//网页title cfg.keyWords=keyWord;//SEO关键字 //使用mustache.js进行页面渲染(参数替换) cfg.OPT=OPT let html = Mustache.render(theme_html,cfg) return new Response(html,{ headers:{ "content-type":"text/html;charset=UTF-8" }, status:200 }) } //渲染前端博客的文章内容页 async function handle_article(id){ // 获取文章信息和内容页模板源码 let currentArticle = await getArticle(id), theme_html = await getThemeHtml("article"), //KV中读取导航栏、分类目录、标签、链接、近期文章等配置信息 menus = await getWidgetMenu(), categories = await getWidgetCategory(), tags = await getWidgetTags(), links = await getWidgetLink(), articles_recently = await getRecentlyArticles(); // 如果文章不存在或已隐藏,返回404 if(!currentArticle || currentArticle.hidden) { return new Response(OPT.html404,{ headers:{ "content-type":"text/html;charset=UTF-8" }, status:404 }); } //获取上篇、本篇、下篇文章 let articles_sibling = await getSiblingArticle(id); //处理文章属性(年月日、url等) processArticleProp(articles_sibling); //获取本篇文章 let article = articles_sibling[1]; //组装文章详情页各参数 let title = article.title.replace(nullToEmpty(OPT.top_flag),'').replace(nullToEmpty(OPT.hidden_flag),'')+" - "+OPT.siteName, keyWord = article.tags.concat(article.category).join(","), cfg = {}; cfg.widgetMenuList = menus,//导航 cfg.widgetCategoryList = categories,//分类目录 cfg.widgetTagsList = tags,//标签 cfg.widgetLinkList = links,//链接 cfg.widgetRecentlyList = articles_recently,//近期文章 cfg.articleOlder = articles_sibling[0]?[articles_sibling[0]]:[],//上篇文章 cfg.articleSingle = article,//本篇文章 cfg.articleNewer = articles_sibling[2]?[articles_sibling[2]]:[],//下篇文章 cfg.title = title,//网页title cfg.keyWords = keyWord;//SEO关键字 //使用mustache.js渲染页面(参数替换) cfg.OPT = OPT let html = Mustache.render(theme_html,cfg) //以html格式返回 return new Response(html,{ headers:{ "content-type":"text/html;charset=UTF-8" }, status:200 }) } //后台请求处理 async function handle_admin(request){ let url = new URL(request.url), paths = url.pathname.trim("/").split("/"), html,//返回html json,//返回json file;//返回文件 //新建页 if(1==paths.length||"list"==paths[1]){ //读取主题的admin/index.html源码 let theme_html=await getThemeHtml("admin/index"), //KV中读取导航栏、分类目录、链接、近期文章等配置信息 categoryJson=await getWidgetCategory(), menuJson=await getWidgetMenu(), linkJson=await getWidgetLink(); //手动替换格式的参数 html = theme_html.replaceHtmlPara("categoryJson",JSON.stringify(categoryJson)) .replaceHtmlPara("menuJson",JSON.stringify(menuJson)) .replaceHtmlPara("linkJson",JSON.stringify(linkJson)) //添加后台首页配置 if(OPT.admin_home_idx && OPT.admin_home_idx>=1 && OPT.admin_home_idx<=4){ html = html.replace("$('#myTab li:eq(0) 1').tab('show')","$($('#myTab a[href*=\"'+location.hash+'\"]')[0]||$('#myTab a:eq("+OPT.admin_home_idx+")')).tab('show')") } //添加置顶样式 if(OPT.top_flag_style){ html += OPT.top_flag_style } //添加隐藏样式 if(OPT.hidden_flag_style){ html += OPT.hidden_flag_style } } //发布 if("publish"==paths[1]){ //KV中获取文章列表 let articles_all=await getAllArticlesList(), tags=[]; //操作标签 //遍历所有文章,汇集所有的tag for(var i=0;i0 && -1==tags.indexOf(articles_all[i].tags[j])){ tags.push(articles_all[i].tags[j]); } } } } console.log(articles_all) //将所有标签一次性写入到KV中,并清除缓存 await saveWidgetTags(JSON.stringify(tags)) json = await purge()?'{"msg":"published ,purge Cache true","rst":true}':'{"msg":"published ,buuuuuuuuuuuut purge Cache false !!!!!!","rst":true}' } //文章列表 if("getList"==paths[1]){ //默认取第一页,每页20篇 let pageNo=(undefined===paths[2]) ? 1 : parseInt(paths[2]), list=await admin_nextPage(pageNo, 20);//每次加载20个 json = JSON.stringify(list) } //修改文章 if("edit"==paths[1]){ let id=paths[2], //获取主题admin/edit源码 theme_html=await getThemeHtml("admin/edit"), //KV中读取分类 categoryJson=JSON.stringify(await getWidgetCategory()), //KV中读取文章内容 articleJson=JSON.stringify(await getArticle(id)); //手动替换格式的参数 html = theme_html.replaceHtmlPara("categoryJson",categoryJson).replaceHtmlPara("articleJson",articleJson.replaceAll("script>","script>")) } //保存配置 if("saveConfig"==paths[1]){ const ret=await parseReq(request); let widgetCategory=ret.WidgetCategory,//分类 widgetMenu=ret.WidgetMenu,//导航 widgetLink=ret.WidgetLink;//链接 //判断格式,写入分类、导航、链接到KV中 if(checkFormat(widgetCategory) && checkFormat(widgetMenu) && checkFormat(widgetLink)){ let success = await saveWidgetCategory(widgetCategory) success = success && await saveWidgetMenu(widgetMenu) success = success && await saveWidgetLink(widgetLink) json = success ? '{"msg":"saved","rst":true}' : '{"msg":"Save Faild!!!","ret":false}' }else{ json = '{"msg":"Not a JSON object","rst":false}' } } //导入 if("import"==paths[1]){ try { const ret = await parseReq(request); let importJson = ret.importJson; console.log("开始导入", typeof importJson); if(checkFormat(importJson)){ let importData = JSON.parse(importJson); let keys = Object.keys(importData); let successCount = 0; let maxId = 0; // 处理系统配置(如果存在) if(importData.OPT) { // 导入分类 if(importData.OPT.widgetCategory) { await saveWidgetCategory(JSON.stringify(importData.OPT.widgetCategory)); } // 导入菜单 if(importData.OPT.widgetMenu) { await saveWidgetMenu(JSON.stringify(importData.OPT.widgetMenu)); } // 导入链接 if(importData.OPT.widgetLink) { await saveWidgetLink(JSON.stringify(importData.OPT.widgetLink)); } // 如果导入的数据中包含SYSTEM_VALUE开头的配置 for(let key of keys) { if(key.startsWith('SYSTEM_VALUE_')) { await saveKV(key, JSON.stringify(importData[key])); } } } // 获取当前文章列表 let currentArticles = await getAllArticlesList() || []; let newArticles = []; // 处理每篇文章 for(let i=0; i maxId) { maxId = currentId; } // 构建文章列表对象 let articleListItem = { id: article.id, title: article.title, img: article.img || '', link: article.link || '', createDate: article.createDate, category: article.category || [], tags: article.tags || [], contentText: article.contentText || '', priority: article.priority || '0.5', top_timestamp: article.top_timestamp || 0, modify_timestamp: article.modify_timestamp || new Date().getTime(), hidden: article.hidden || 0, changefreq: article.changefreq || 'daily' }; newArticles.push(articleListItem); successCount++; } catch(e) { console.error("导入单篇文章失败:", article.id, e); } } // 合并文章列表并去重 let mergedArticles = currentArticles.filter(current => !newArticles.some(newArticle => newArticle.id === current.id) ); mergedArticles = mergedArticles.concat(newArticles); // 排序文章列表 mergedArticles = sortArticle(mergedArticles); // 保存更新后的文章列表 await saveArticlesList(JSON.stringify(mergedArticles)); // 获取当前的序号 let currentIndexNum = await getIndexNum(); currentIndexNum = parseInt(currentIndexNum); // 确保maxId大于当前序号 if(!isNaN(currentIndexNum) && currentIndexNum > maxId) { maxId = currentIndexNum; } // 保存新的序号 console.log("更新文章序号为:", maxId); await saveIndexNum(maxId); // 清理缓存 await purge(); json = JSON.stringify({ status: "success", message: `成功导入 ${successCount} 篇文章,系统配置已更新`, totalArticles: mergedArticles.length, newIndexNum: maxId }); } else { json = JSON.stringify({ status: "error", message: "导入的JSON格式不正确" }); } } catch(e) { console.error("导入处理出错:", e); json = JSON.stringify({ status: "error", message: "导入处理失败: " + e.message }); } } //导出 if("export"===paths[1]){ console.log("开始导出"); async function exportArticle(arr=[],cursor="",limit=1){ //分页获取文章内容 const list=await CFBLOG.list({limit:limit,cursor:cursor}); if(!1 in list) return {}; arr=arr.concat(list.keys) console.log("导出: ",typeof list, JSON.stringify(list)) //判断是否导出完毕 if(list.list_complete){ let ret = {OPT:OPT}; for(let i=0;i"; let article = await getArticle(articles_all[i].id); if(null != article){ xml+="\n\t\t"+article.contentMD.replaceAll('<','<').replaceAll('>','>').replaceAll('&','&')+"" } xml+="\n\t\thttps://"+OPT.siteDomain+"/article/"+articles_all[i].id+"/"+articles_all[i].link+".html", xml+="\n\t\t", xml+="\n\t"; } xml+="\n" file = { name: "search.xml", content: xml } } //导出sitemap.xml if("sitemap.xml"===paths[1]){ console.log("开始导出"); //读取文章列表,并按照特定的xml格式进行组装 let articles_all=await getArticlesList() let xml='\n'; for(var i=0;i", xml+="\n\t\t"+articles_all[i].createDate.substr(0,10)+"", xml+="\n\t\t"+(void 0===articles_all[i].changefreq?"daily":articles_all[i].changefreq)+"", xml+="\n\t\t"+(void 0===articles_all[i].priority?"0.5":articles_all[i].priority)+"", xml+="\n\t"; } xml+="\n" file = { name: "sitemap.xml", content: xml } } //新建文章 if("saveAddNew"==paths[1]){ const ret=await parseReq(request); let title=ret.title,//文章标题 img=ret.img,//插图 link=ret.link,//永久链接 createDate=ret.createDate.replace('T',' '),//发布日期 category=ret.category,//分类 tags=ret.tags,//标签 priority=void 0===ret.priority?"0.5":ret.priority,//权重 changefreq=void 0===ret.changefreq?"daily":ret.changefreq,//更新频率 contentMD=ret["content-markdown-doc"],//文章内容-md格式 contentHtml=ret["content-html-code"],//文章内容-html格式 contentText="",//文章摘要 top_timestamp=ret.top_timestamp*1,//置顶时间戳,不置顶时为0 modify_timestamp=new Date().getTime()+8*60*60*1000,//修改时间戳 hidden=ret.hidden*1,//是否隐藏 id="";//文章id //校验参数完整性 if(title.length>0 && createDate.length>0 && category.length>0 && contentMD.length>0 && contentHtml.length>0){ id=await generateId(), contentText=contentHtml.replace(/<\/?[^>]*>/g,"").trim().substring(0,OPT.readMoreLength);//摘要 //组装文章json let article={ id:id, title:title, img:img, link:link, createDate:createDate, category:category, tags:tags, contentMD:contentMD, contentHtml:contentHtml, contentText:contentText, priority:priority, top_timestamp:top_timestamp, modify_timestamp:modify_timestamp, hidden:hidden, changefreq:changefreq }; //将文章json写入KV(key为文章id,value为文章json字符串) await saveArticle(id,JSON.stringify(article)); //组装文章json let articleWithoutHtml={ id:id, title:title, img:img, link:link, createDate:createDate, category:category, tags:tags, contentText:contentText, priority:priority, top_timestamp:top_timestamp, modify_timestamp:modify_timestamp, hidden:hidden, changefreq:changefreq }, articles_all_old=await getAllArticlesList(),//读取文章列表 articles_all=[]; //将最新的文章写入文章列表中,并按id排序后,再次回写到KV中 articles_all.push(articleWithoutHtml), articles_all=articles_all.concat(articles_all_old), articles_all=sortArticle(articles_all), await saveArticlesList(JSON.stringify(articles_all)) json = '{"msg":"added OK","rst":true,"id":"'+id+'"}' }else{ json = '{"msg":"信息不全","rst":false}' } } //删除 if("delete"==paths[1]){ let id=paths[2] if(6==id.length){ try { // 1. 首先删除文章内容 await CFBLOG.delete(id); console.log(`文章内容删除成功: ${id}`); // 2. 获取并更新文章列表 let articles = await getAllArticlesList(); let originalLength = articles.length; articles = articles.filter(article => article.id !== id); if(articles.length < originalLength){ // 3. 保存更新后的文章列表 let saveResult = await saveArticlesList(JSON.stringify(articles)); console.log(`文章列表更新结果: ${saveResult}`); // 4. 更新文章序号 let indexNum = articles.length > 0 ? Math.max(...articles.map(a => parseInt(a.id))) : 0; await saveIndexNum(indexNum); console.log(`文章序号已更新为: ${indexNum}`); // 5. 强制清除缓存 let purgeResult = await purge(); console.log(`缓存清除结果: ${purgeResult}`); // 6. 返回成功响应 json = JSON.stringify({ status: "success", message: "文章删除成功", id: id, purged: purgeResult, newIndexNum: indexNum }); } else { json = JSON.stringify({ status: "error", message: "文章在列表中未找到", id: id }); } } catch(e) { console.error("删除文章时出错:", e); json = JSON.stringify({ status: "error", message: "删除文章时发生错误: " + e.message, id: id }); } } else { json = JSON.stringify({ status: "error", message: "无效的文章ID", id: id }); } } //保存编辑的文章 if("saveEdit"==paths[1]){ const ret=await parseReq(request); let title=ret.title,//文章标题 img=ret.img,//插图 link=ret.link,//永久链接 createDate=ret.createDate.replace('T',' '),//发布日期 category=ret.category,//分类 tags=ret.tags,//标签 priority=void 0===ret.priority?"0.5":ret.priority,//权重 changefreq=void 0===ret.changefreq?"daily":ret.changefreq,//更新频率 contentMD=ret["content-markdown-doc"],//文章内容-md格式 contentHtml=ret["content-html-code"],//文章内容-html格式 contentText="",//文章摘要 top_timestamp=ret.top_timestamp*1,//置顶则设置时间戳,不置顶时为0 modify_timestamp=new Date().getTime()+8*60*60*1000,//修改时间戳 hidden=ret.hidden*1,//是否隐藏 id=ret.id;//文章id //校验参数完整性 if(title.length>0 && createDate.length>0 && category.length>0 && contentMD.length>0 && contentHtml.length>0){ contentText=contentHtml.replace(/<\/?[^>]*>/g,"").trim().substring(0,OPT.readMoreLength);//摘要 //组装文章json let article={ id:id, title:title, img:img, link:link, createDate:createDate, category:category, tags:tags, contentMD:contentMD, contentHtml:contentHtml, contentText:contentText, priority:priority, top_timestamp:top_timestamp, modify_timestamp:modify_timestamp, hidden:hidden, changefreq:changefreq }; //将文章json写入KV(key为文章id,value为文章json字符串) await saveArticle(id,JSON.stringify(article)); //组装文章json let articleWithoutHtml={ id:id, title:title, img:img, link:link, createDate:createDate, category:category, tags:tags, contentText:contentText, priority:priority, top_timestamp:top_timestamp, modify_timestamp:modify_timestamp, hidden:hidden, changefreq:changefreq }, articles_all=await getAllArticlesList();//读取文章列表 //console.log(articles_all) //将原对象删掉,将最新的文章加入文章列表中,并重新按id排序后,再次回写到KV中 for(var i=articles_all.length-1;i>=0;i--){//按索引删除,要倒序,否则索引值会变 if(articles_all[i].id == id){ articles_all.splice(i,1); break; } } articles_all.push(articleWithoutHtml), articles_all=sortArticle(articles_all), await saveArticlesList(JSON.stringify(articles_all)) json = '{"msg":"Edit OK","rst":true,"id":"'+id+'"}' }else{ json = '{"msg":"信息不全","rst":false}' } } //返回结果 if(!json &&!html && !file){ json = '{"msg":"some errors","rst":false}' } if(file){ return new Response(file.content,{ headers:{ "content-type":"application/octet-stream;charset=utf-8", "Content-Disposition":"attachment; filename="+file.name }, status:200 }) } if(html){ return new Response(html,{ headers:{ "content-type":"text/html;charset=UTF-8" }, status:200 }) } if(json){ return new Response(json ,{ headers:{ "content-type":"application/json;charset=UTF-8" }, status:200 }) } } /**------【④.抽丝剥茧,抽取公用的业务方法】-----**/ //访问管理后台或私密博客,则进行Base Auth function parseBasicAuth(request){ const auth = request.headers.get("Authorization"); if(!auth || !/^Basic [A-Za-z0-9._~+/-]+=*$/i.test(auth)){ const token = request.headers.get("cfblog_token"); if(token){ // 增加token过期检查 try { const tokenData = JSON.parse(atob(token.split('.')[1])); if(tokenData.exp < Date.now()/1000) { return false; } } catch(e) { return false; } let url = new URL(request.url) let paths = url.pathname.trim("/").split("/") if("admin"==paths[0] && ("search.xml"==paths[1]||"sitemap.xml"==paths[1])){ return token === ACCOUNT.third_token } } return false; } const[user, pwd] = atob(auth.split(" ").pop()).split(":"); return user === ACCOUNT.user && pwd === ACCOUNT.password } // 添加登录页面处理函数 async function handle_login(request) { const url = new URL(request.url); // 处理登录表单提交 if (request.method === "POST") { const formData = await request.formData(); const username = formData.get("username"); const password = formData.get("password"); if (username === ACCOUNT.user && password === ACCOUNT.password) { // 登录成功,生成一个简单的token(实际应用中应使用更安全的方法) const token = btoa(username + ":" + password); // 重定向到管理页面,带上Authorization头 return new Response("Login successful", { status: 302, headers: { "Set-Cookie": `auth=${token}; HttpOnly; Path=/admin; SameSite=Strict`, "Location": "/admin" } }); } else { // 登录失败,返回登录页面并显示错误 return renderLoginPage(true); } } // 显示登录页面 return renderLoginPage(false); } // 渲染登录页面函数 function renderLoginPage(showError) { const errorMessage = showError ? '
用户名或密码错误
' : ''; const html = ` 管理员登录 - ${OPT.siteName} `; return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8" } }); } //获取所有【公开】文章:仅前台使用 async function getArticlesList(){ let articles_all = await getAllArticlesList(); // 过滤掉隐藏的文章 return articles_all.filter(article => { // 确保article存在且hidden属性为0或undefined/null return article && !article.hidden; }); } //文章排序:先按id倒排,再按置顶时间倒排 function sortArticle(articles){ return sort(sort(articles,'id'),'top_timestamp'); } //获取近期文章列表 async function getRecentlyArticles(articles){ if(!articles){ articles = await getArticlesList(); } if(OPT.recentlyType == 2){//按修改时间倒序 articles = sort([].concat(articles),'modify_timestamp'); } let articles_recently = articles.slice(0,OPT.recentlySize); for(var i=0;i String.prototype.replaceHtmlPara=function(t,e){ return null!=e&&(e=e.replace(new RegExp("[$]","g"),"$$$$")),this.replace(new RegExp("\x3c!--{"+t+"}--\x3e","g"),e) } //replaceAll 替换全部 String.prototype.replaceAll=function(t,e){ return this.replace(new RegExp(t,"g"),e) } //小于2位,前面补个0 function pad(t){ return t>=0&&t<=9?"0"+t:t } //排序(默认倒序) function sort(arr, field, desc=true){ return arr.sort((function(m,n){ var a=m[field]||'0', b=n[field]||'0'; return desc?(a>b?-1:(ab?1:0)) })) } //undefined转空字符串 function nullToEmpty(k){ return k==undefined?'':k } //判断格式:字符串是否为json,或者参数是否为对象 function checkFormat(t){ if("string"==typeof t){ try{ var e=JSON.parse(t); return !("object"!=typeof e||!e) }catch(t){ return false } } return !("object"!=typeof t||!t) } //引入mustache.js,4.1.0:https://cdn.bootcdn.net/ajax/libs/mustache.js/4.1.0/mustache.min.js (function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.Mustache=factory())})(this,function(){"use strict";var objectToString=Object.prototype.toString;var isArray=Array.isArray||function isArrayPolyfill(object){return objectToString.call(object)==="[object Array]"};function isFunction(object){return typeof object==="function"}function typeStr(obj){return isArray(obj)?"array":typeof obj}function escapeRegExp(string){return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function hasProperty(obj,propName){return obj!=null&&typeof obj==="object"&&propName in obj}function primitiveHasOwnProperty(primitive,propName){return primitive!=null&&typeof primitive!=="object"&&primitive.hasOwnProperty&&primitive.hasOwnProperty(propName)}var regExpTest=RegExp.prototype.test;function testRegExp(re,string){return regExpTest.call(re,string)}var nonSpaceRe=/\S/;function isWhitespace(string){return!testRegExp(nonSpaceRe,string)}var entityMap={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function escapeHtml(string){return String(string).replace(/[&<>"'`=\/]/g,function fromEntityMap(s){return entityMap[s]})}var whiteRe=/\s*/;var spaceRe=/\s+/;var equalsRe=/\s*=/;var curlyRe=/\s*\}/;var tagRe=/#|\^|\/|>|\{|&|=|!/;function parseTemplate(template,tags){if(!template)return[];var lineHasNonSpace=false;var sections=[];var tokens=[];var spaces=[];var hasTag=false;var nonSpace=false;var indentation="";var tagIndex=0;function stripSpace(){if(hasTag&&!nonSpace){while(spaces.length)delete tokens[spaces.pop()]}else{spaces=[]}hasTag=false;nonSpace=false}var openingTagRe,closingTagRe,closingCurlyRe;function compileTags(tagsToCompile){if(typeof tagsToCompile==="string")tagsToCompile=tagsToCompile.split(spaceRe,2);if(!isArray(tagsToCompile)||tagsToCompile.length!==2)throw new Error("Invalid tags: "+tagsToCompile);openingTagRe=new RegExp(escapeRegExp(tagsToCompile[0])+"\\s*");closingTagRe=new RegExp("\\s*"+escapeRegExp(tagsToCompile[1]));closingCurlyRe=new RegExp("\\s*"+escapeRegExp("}"+tagsToCompile[1]))}compileTags(tags||mustache.tags);var scanner=new Scanner(template);var start,type,value,chr,token,openSection;while(!scanner.eos()){start=scanner.pos;value=scanner.scanUntil(openingTagRe);if(value){for(var i=0,valueLength=value.length;i"){token=[type,value,start,scanner.pos,indentation,tagIndex,lineHasNonSpace]}else{token=[type,value,start,scanner.pos]}tagIndex++;tokens.push(token);if(type==="#"||type==="^"){sections.push(token)}else if(type==="/"){openSection=sections.pop();if(!openSection)throw new Error('Unopened section "'+value+'" at '+start);if(openSection[1]!==value)throw new Error('Unclosed section "'+openSection[1]+'" at '+start)}else if(type==="name"||type==="{"||type==="&"){nonSpace=true}else if(type==="="){compileTags(value)}}stripSpace();openSection=sections.pop();if(openSection)throw new Error('Unclosed section "'+openSection[1]+'" at '+scanner.pos);return nestTokens(squashTokens(tokens))}function squashTokens(tokens){var squashedTokens=[];var token,lastToken;for(var i=0,numTokens=tokens.length;i0?sections[sections.length-1][4]:nestedTokens;break;default:collector.push(token)}}return nestedTokens}function Scanner(string){this.string=string;this.tail=string;this.pos=0}Scanner.prototype.eos=function eos(){return this.tail===""};Scanner.prototype.scan=function scan(re){var match=this.tail.match(re);if(!match||match.index!==0)return"";var string=match[0];this.tail=this.tail.substring(string.length);this.pos+=string.length;return string};Scanner.prototype.scanUntil=function scanUntil(re){var index=this.tail.search(re),match;switch(index){case-1:match=this.tail;this.tail="";break;case 0:match="";break;default:match=this.tail.substring(0,index);this.tail=this.tail.substring(index)}this.pos+=match.length;return match};function Context(view,parentContext){this.view=view;this.cache={".":this.view};this.parent=parentContext}Context.prototype.push=function push(view){return new Context(view,this)};Context.prototype.lookup=function lookup(name){var cache=this.cache;var value;if(cache.hasOwnProperty(name)){value=cache[name]}else{var context=this,intermediateValue,names,index,lookupHit=false;while(context){if(name.indexOf(".")>0){intermediateValue=context.view;names=name.split(".");index=0;while(intermediateValue!=null&&index")value=this.renderPartial(token,context,partials,config);else if(symbol==="&")value=this.unescapedValue(token,context);else if(symbol==="name")value=this.escapedValue(token,context,config);else if(symbol==="text")value=this.rawValue(token);if(value!==undefined)buffer+=value}return buffer};Writer.prototype.renderSection=function renderSection(token,context,partials,originalTemplate,config){var self=this;var buffer="";var value=context.lookup(token[1]);function subRender(template){return self.render(template,context,partials,config)}if(!value)return;if(isArray(value)){for(var j=0,valueLength=value.length;j0||!lineHasNonSpace)){partialByNl[i]=filteredIndentation+partialByNl[i]}}return partialByNl.join("\n")};Writer.prototype.renderPartial=function renderPartial(token,context,partials,config){if(!partials)return;var tags=this.getConfigTags(config);var value=isFunction(partials)?partials(token[1]):partials[token[1]];if(value!=null){var lineHasNonSpace=token[6];var tagIndex=token[5];var indentation=token[4];var indentedValue=value;if(tagIndex==0&&indentation){indentedValue=this.indentPartial(value,indentation,lineHasNonSpace)}var tokens=this.parse(indentedValue,tags);return this.renderTokens(tokens,context,partials,indentedValue,config)}};Writer.prototype.unescapedValue=function unescapedValue(token,context){var value=context.lookup(token[1]);if(value!=null)return value};Writer.prototype.escapedValue=function escapedValue(token,context,config){var escape=this.getConfigEscape(config)||mustache.escape;var value=context.lookup(token[1]);if(value!=null)return typeof value==="number"&&escape===mustache.escape?String(value):escape(value)};Writer.prototype.rawValue=function rawValue(token){return token[1]};Writer.prototype.getConfigTags=function getConfigTags(config){if(isArray(config)){return config}else if(config&&typeof config==="object"){return config.tags}else{return undefined}};Writer.prototype.getConfigEscape=function getConfigEscape(config){if(config&&typeof config==="object"&&!isArray(config)){return config.escape}else{return undefined}};var mustache={name:"mustache.js",version:"4.1.0",tags:["{{","}}"],clearCache:undefined,escape:undefined,parse:undefined,render:undefined,Scanner:undefined,Context:undefined,Writer:undefined,set templateCache(cache){defaultWriter.templateCache=cache},get templateCache(){return defaultWriter.templateCache}};var defaultWriter=new Writer;mustache.clearCache=function clearCache(){return defaultWriter.clearCache()};mustache.parse=function parse(template,tags){return defaultWriter.parse(template,tags)};mustache.render=function render(template,view,partials,config){if(typeof template!=="string"){throw new TypeError('Invalid template! Template should be a "string" '+'but "'+typeStr(template)+'" was given as the first '+"argument for mustache#render(template, view, partials)")}return defaultWriter.render(template,view,partials,config)};mustache.escape=escapeHtml;mustache.Scanner=Scanner;mustache.Context=Context;mustache.Writer=Writer;return mustache}); // 添加删除文章的功能 async function deleteArticle(id) { try { // 获取文章列表 let articles = await getAllArticlesList(); // 查找要删除的文章索引 let index = -1; for(let i = 0; i < articles.length; i++) { if(articles[i].id === id) { index = i; break; } } // 如果找到文章,从列表中删除 if(index !== -1) { // 从列表中删除文章 articles.splice(index, 1); // 更新文章列表 await saveArticlesList(articles); // 删除文章内容 await CFBLOG.delete(id); // 重新计算最新文章序号 let maxId = articles.reduce((max, article) => Math.max(max, parseInt(article.id)), 0); await saveIndexNum(maxId); return new Response(JSON.stringify({ status: "success", message: "文章已成功删除", indexNum: maxId }), { headers: { "content-type": "application/json" } }); } else { return new Response(JSON.stringify({ status: "error", message: "未找到指定文章" }), { headers: { "content-type": "application/json" }, status: 404 }); } } catch(e) { return new Response(JSON.stringify({ status: "error", message: "删除文章失败:" + e.message }), { headers: { "content-type": "application/json" }, status: 500 }); } } // 修改后台HTML页面,在文章列表中添加删除按钮 // 找到admin/index.html文件中处理的getHtmlIndex函数,添加删除按钮和交互脚本 function getHtmlIndex(){ // ... 现有代码 ... // 在页面末尾添加删除功能的JavaScript代码 html = html.replace("", ` `); return html; } // 统一命名规范 const SYSTEM_KEYS = { ARTICLE_LIST: 'SYSTEM_INDEX_LIST', ARTICLE_NUM: 'SYSTEM_INDEX_NUM', WIDGET_MENU: 'SYSTEM_VALUE_WidgetMenu', WIDGET_CATEGORY: 'SYSTEM_VALUE_WidgetCategory', WIDGET_TAGS: 'SYSTEM_VALUE_WidgetTags', WIDGET_LINK: 'SYSTEM_VALUE_WidgetLink' }; // 提取配置常量 const CONFIG = { PAGE_SIZE: 20, CACHE_TIME: 60 * 60 * 24 * 2, READ_MORE_LENGTH: 150 }; // 使用async/await替代Promise链 async function purge(cacheZoneId=ACCOUNT.cacheZoneId, cacheToken=ACCOUNT.cacheToken) { try { if(!cacheZoneId || !cacheToken || cacheZoneId.length<5 || cacheToken.length<5) { throw new Error('缺少有效的cacheZoneId或cacheToken'); } const response = await fetch( `https://api.cloudflare.com/client/v4/zones/${cacheZoneId}/purge_cache`, { method: 'POST', headers: { 'Authorization': `Bearer ${cacheToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({purge_everything: true}) } ); const result = await response.json(); if(!result.success) { throw new Error(result.errors.join(', ')); } return true; } catch(e) { console.error('清除缓存失败:', e); return false; } }