⚠ 警告: 请更新twikoo版本,本代码已合并至twikoo新版本中 ~
环境说明 博客框架:Hexo Fluid 主题 评论系统:Twikoo 1.6.44(Vercel 部署) 图床:MoePic 提供的 Chevereto V4 图床 核心思路 Twikoo 1.6.44 的前端图片上传逻辑完全由后端控制。Twikoo 后端内置支持的图床有限,不支持 Chevereto。
因此方案分两步:
在 Vercel 上创建一个代理接口 /api/upload,负责接收前端图片并转发到 Chevereto V4 在前端直接 hook Twikoo 的 Vue 组件方法 onSelectImage,拦截图片上传,改为调用自己的代理接口 第一步:创建 Vercel 代理接口 在你的 Twikoo Vercel 项目根目录创建 api/upload.js:
点击展开代码 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 const axios = require ('axios' );const FormData = require ('form-data' );const Busboy = require ('busboy' );module .exports = async (req, res) => { res.setHeader ('Access-Control-Allow-Origin' , '*' ); res.setHeader ('Access-Control-Allow-Methods' , 'POST, OPTIONS' ); res.setHeader ('Access-Control-Allow-Headers' , 'Content-Type' ); if (req.method === 'OPTIONS' ) return res.status (200 ).end (); if (req.method !== 'POST' ) return res.status (405 ).json ({ error : 'Method not allowed' }); const CHEVERETO_URL = process.env .CHEVERETO_URL ; const CHEVERETO_KEY = process.env .CHEVERETO_API_KEY ; return new Promise ((resolve ) => { const busboy = Busboy ({ headers : req.headers }); let fileData = null ; busboy.on ('file' , (fieldname, file, info ) => { const { filename, mimeType } = info; const chunks = []; file.on ('data' , (chunk ) => chunks.push (chunk)); file.on ('end' , () => { fileData = { buffer : Buffer .concat (chunks), filename : filename || 'image.jpg' , mimeType : mimeType || 'image/jpeg' , }; }); }); busboy.on ('finish' , async () => { if (!fileData) { res.status (400 ).json ({ error : '没有收到文件' }); return resolve (); } try { const form = new FormData (); form.append ('source' , fileData.buffer , { filename : fileData.filename , contentType : fileData.mimeType , knownLength : fileData.buffer .length , }); form.append ('format' , 'json' ); const response = await axios.post ( `${CHEVERETO_URL} /api/1/upload` , form, { headers : { ...form.getHeaders (), 'X-API-Key' : CHEVERETO_KEY , }, timeout : 20000 , maxContentLength : Infinity , maxBodyLength : Infinity , } ); const imageUrl = response.data ?.image ?.url ; if (!imageUrl) { res.status (500 ).json ({ error : '未返回图片URL' , detail : response.data }); return resolve (); } res.status (200 ).json ({ url : imageUrl }); resolve (); } catch (err) { if (err.response ) { res.status (500 ).json ({ error : err.message , chevereto_status : err.response .status , chevereto_detail : err.response .data , }); } else { res.status (500 ).json ({ error : err.message }); } resolve (); } }); busboy.on ('error' , (err ) => { res.status (500 ).json ({ error : 'Busboy错误: ' + err.message }); resolve (); }); req.pipe (busboy); }); };
添加依赖 确认 package.json 中包含以下依赖:
1 2 3 4 5 6 7 8 { "dependencies" : { "twikoo-vercel" : "latest" , "axios" : "^1.6.0" , "busboy" : "^1.6.0" , "form-data" : "^4.0.0" } }
配置 Vercel 环境变量 在 Vercel → 你的项目 → Settings → Environment Variables 中添加:
变量名 值 CHEVERETO_URLhttps://你的图床域名(不带末尾斜杠)CHEVERETO_API_KEY你的 Chevereto V4 API Key
⚠️ 注意 :不要设置 IMAGE_CDN、IMAGE_CDN_URL、IMAGE_CDN_TOKEN 这三个环境变量,否则 Twikoo 会启用内置图床逻辑并报错。
获取 Chevereto V4 API Key 登录 Chevereto 后台 → Dashboard → Settings → API,确认 API 已启用,复制 API V1 Key。
第二步:Hook 前端 Vue 组件 为什么不能用普通 DOM 事件拦截 Twikoo 的上传按钮是 Vue 组件渲染的,使用 on:{change:e.onSelectImage} 绑定事件。即使用 cloneNode 替换元素,Vue 仍会重新绑定事件。必须直接覆盖 Vue 实例上的 onSelectImage 方法。
创建注入脚本 在 Hexo 博客根目录的 scripts/ 文件夹下创建 inject-twikoo.js:
点击展开代码 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 hexo.extend .injector .register ('body_end' , ` <script> (function() { var UPLOAD_API = 'https://你的twikoo.vercel.app/api/upload'; function hookVueComponent() { var input = document.querySelector('input.tk-input-image'); if (!input) { setTimeout(hookVueComponent, 500); return; } // 向上遍历父元素,找到有 onSelectImage 方法的 Vue 实例 var el = input; while (el) { var vue = el.__vue__; if (vue && typeof vue.onSelectImage === 'function') { console.log('找到 Vue 实例,准备 hook onSelectImage'); vue.onSelectImage = async function(e) { var file = e.target.files[0]; if (!file) return; try { var fd = new FormData(); fd.append('file', file); var res = await fetch(UPLOAD_API, { method: 'POST', body: fd }); var data = await res.json(); if (!data.url) throw new Error(data.error || '未返回URL'); // 将图片 Markdown 插入评论框 var textarea = document.querySelector('.tk-input .el-textarea__inner') || document.querySelector('.tk-input textarea'); if (textarea) { var pos = textarea.selectionStart !== undefined ? textarea.selectionStart : textarea.value.length; var imgMd = ''; var val = textarea.value; textarea.value = val.slice(0, pos) + imgMd + val.slice(pos); textarea.dispatchEvent(new Event('input', { bubbles: true })); } console.log('图片上传成功:', data.url); } catch(err) { console.error('上传失败:', err); alert('图片上传失败: ' + err.message); } e.target.value = ''; }; console.log('Twikoo Vue onSelectImage 已接管'); return; } el = el.parentElement; } setTimeout(hookVueComponent, 500); } // 等待评论区 DOM 出现后再 hook var observer = new MutationObserver(function() { if (document.querySelector('input.tk-input-image')) { observer.disconnect(); setTimeout(hookVueComponent, 300); } }); observer.observe(document.body, { childList: true, subtree: true }); })(); </script> ` , 'default' );
注意 :将 https://你的twikoo.vercel.app/api/upload 替换为你的 Vercel 域名。
第三步:部署 这就不用我废话了吧,祝你成功!
我踩的坑 坑1:Chevereto V4 认证方式变了 V3 用表单字段 key 认证,V4 改为 HTTP Header X-API-Key,混用会返回 400。
坑2:base64 传输会损坏 用 URLSearchParams 传 base64 Data URL 时,+ 和 / 字符被 URL 编码破坏,导致 Chevereto 报 Invalid base64 string。应直接用 multipart 传二进制。
坑3:Twikoo 内置图床环境变量干扰 设置了 IMAGE_CDN=lskypro 等变量后,Twikoo 后端会用内置逻辑处理上传,完全绕过自定义代理,且会把 IMAGE_CDN_URL 的值当成 Bearer Token 发出去。。
坑4:cloneNode 无法移除 Vue 事件 Twikoo 使用 Vue 绑定 change 事件,cloneNode 替换元素后 Vue 会重新渲染并重新绑定,必须直接覆盖 Vue 实例的 onSelectImage 方法。