Vercel 部署的 Twikoo 对接自定义 Chevereto V4 图床

环境说明

  • 博客框架:Hexo Fluid 主题
  • 评论系统:Twikoo 1.6.44(Vercel 部署)
  • 图床:MoePic 提供的 Chevereto V4 图床

核心思路

Twikoo 1.6.44 的前端图片上传逻辑完全由后端控制。Twikoo 后端内置支持的图床有限,不支持 Chevereto。

因此方案分两步:

  1. 在 Vercel 上创建一个代理接口 /api/upload,负责接收前端图片并转发到 Chevereto V4
  2. 在前端直接 hook Twikoo 的 Vue 组件方法 onSelectImage,拦截图片上传,改为调用自己的代理接口

第一步:创建 Vercel 代理接口

在你的 Twikoo Vercel 项目根目录创建 api/upload.js

在github创建js

编辑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 {
// Chevereto V4 使用 multipart 直接传二进制
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, // V4 用 Header 认证
},
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_CDNIMAGE_CDN_URLIMAGE_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 = '![](' + data.url + ')';
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 方法。


Vercel 部署的 Twikoo 对接自定义 Chevereto V4 图床
http://www.linzekai.cn/posts/db3e042c/
作者
林泽凯
发布于
2026年2月22日
许可协议