## 环境说明
- 博客框架:Hexo Fluid 主题
- 评论系统:Twikoo 1.6.44(Vercel 部署)
- 图床:[MoePic](https://loveloli.me/) 提供的 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`:


<details>
<summary>点击展开代码</summary>
```javascript
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);
});
};
```
</details>
### 添加依赖
确认 `package.json` 中包含以下依赖:
```json
{
"dependencies": {
"twikoo-vercel": "latest",
"axios": "^1.6.0",
"busboy": "^1.6.0",
"form-data": "^4.0.0"
}
}
```
### 配置 Vercel 环境变量
在 Vercel → 你的项目 → Settings → Environment Variables 中添加:
| 变量名 | 值 |
|---|---|
| `CHEVERETO_URL` | `https://你的图床域名`(不带末尾斜杠) |
| `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`:
<details>
<summary>点击展开代码</summary>
```javascript
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');
```
</details>
**注意**:将 `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` 方法。