# 1. 问题现象 Typecho 升级到 `1.3` 后,Handsome 后台编辑器切换为 `Vditor`,直接复制或粘贴图片时上传失败。 常见报错: ```text write-post.php:2108 Uncaught ReferenceError: plupload is not defined ``` 浏览器 Network 中可能看到: ```text /action/multi-upload?do=uploadfile... ``` 接口返回: ```json { "msg": "图片外链格式不正确", "code": 100, "data": { "url": "", "title": "", "isImage": false } } ``` --- # 2. 问题原因 Handsome 的 Vditor 上传逻辑仍然兼容旧版 Typecho 后台上传方式。 Typecho 1.3 后: - 后台上传不再依赖 `plupload`; - 原生上传接口是 `/action/upload`; - 官方上传逻辑使用 `fetch + FormData`; - Typecho 原生上传字段名是 `file`; - Handsome 的 `/action/multi-upload?do=uploadfile` 更偏向处理外链图片; - Vditor 粘贴图片上传的是二进制文件,不是外链 URL。 所以会出现两个问题: 1. 上传失败时,旧代码引用 `plupload`,导致 JS 报错; 2. Vditor 的二进制图片被 Handsome 的 `multi-upload` 当成外链图片处理,返回“图片外链格式不正确”。 --- # 3. 推荐修复方案 这里推荐使用: ```text 保留 Handsome 的 multi-upload,但在收到二进制文件时转交 Typecho 原生上传 ``` 也就是: ```text Vditor 粘贴图片 / 拖拽上传文件 => /action/multi-upload?do=uploadfile => Action.php 检测到 $_FILES => 转交 Typecho 原生 Widget_Upload => 返回 Typecho 原生上传结果 ``` 外链图片转存仍然保留 Handsome 原逻辑: ```text 外链图片转存 => /action/multi-upload?do=uploadfile => 没有 $_FILES => 继续走 Handsome_Upload => 返回 Handsome 的 {code, msg, data} ``` 这样既修复 Vditor 粘贴上传,又不会破坏 Handsome 原来的外链图片处理能力。 --- # 4. 需要修改的文件 主要修改两个文件: ```text /usr/plugins/Handsome/Action.php /usr/plugins/Handsome/assets/js/origin/editor-js.php ``` 修改前务必备份。 --- # 5. 修复 `plupload is not defined` 继续在: ```text /usr/plugins/Handsome/assets/js/origin/editor-js.php ``` 找到类似下面的函数: ```js function fileUploadError(error) { var file = error.file, code = error.code, word; switch (code) { case plupload.FILE_SIZE_ERROR: word = '文件大小超过限制'; break; case plupload.FILE_EXTENSION_ERROR: word = '文件扩展名不被支持'; break; case plupload.FILE_DUPLICATE_ERROR: word = '文件已经上传过'; break; case plupload.HTTP_ERROR: default: word = '上传出现错误'; break; } var fileError = '%s 上传失败'.replace('%s', file.name), li, exist = $('#' + file.id); if (exist.length > 0) { li = exist.removeClass('loading').html(fileError); } else { li = $('' + fileError + '' + word + '').appendTo('#file-list'); } li.effect('highlight', {color: '#FBC2C4'}, 2000, function () { $(this).remove(); }); try { this.removeFile(file); } catch (e) { } } ``` 替换为: ```js function fileUploadError(error) { var file = error.file || {}; var code = error.code || error.type || ''; var word = '上传出现错误'; switch (code) { case 'size': case 'FILE_SIZE_ERROR': case -600: word = '文件大小超过限制'; break; case 'type': case 'FILE_EXTENSION_ERROR': case -601: word = '文件扩展名不被支持'; break; case 'duplicate': case 'FILE_DUPLICATE_ERROR': case -602: word = '文件已经上传过'; break; case 'network': case 'HTTP_ERROR': case -200: default: word = '上传出现错误'; break; } // 允许 format() 传入更具体的错误提示,例如 Typecho 返回 false 时的格式不支持提示 if (error.message) { word = error.message; } file.id = file.id || ('upload-error-' + Date.now()); file.name = file.name || '文件'; var fileError = '%s 上传失败'.replace('%s', file.name), li, exist = $('#' + file.id); if (exist.length > 0) { li = exist.removeClass('loading').html(fileError + '' + word); } else { li = $('' + fileError + '' + word + '').appendTo('#file-list'); } li.effect('highlight', {color: '#FBC2C4'}, 2000, function () { $(this).remove(); }); } ``` --- # 6. 修改 `Action.php`:让 multi-upload 支持 Vditor 二进制上传 打开: ```text /usr/plugins/Handsome/Action.php ``` 找到: ```php public function uploadApi() { if (class_exists("Handsome_Upload")) { $reflectionWidget = new ReflectionClass("Handsome_Upload"); if ($reflectionWidget->implementsInterface('Widget_Interface_Do')) { $this->widget("Handsome_Upload")->action(); } } } ``` 替换为: ```php public function uploadApi() { // Vditor 粘贴/拖拽上传的是二进制文件,交给 Typecho 原生上传 if (!empty($_FILES)) { $this->widget('Widget_Upload')->action(); return; } // 没有 $_FILES 的情况,保留 Handsome 原来的外链图片转存逻辑 if (class_exists("Handsome_Upload")) { $reflectionWidget = new ReflectionClass("Handsome_Upload"); if ($reflectionWidget->implementsInterface('Widget_Interface_Do')) { $this->widget("Handsome_Upload")->action(); } } } ``` 说明: - Vditor 粘贴图片时提交的是 `multipart/form-data`,文件会进入 `$_FILES`; - 检测到 `$_FILES` 后,直接交给 Typecho 原生 `Widget_Upload` 处理; - 没有 `$_FILES` 时,继续走 Handsome 原来的 `Handsome_Upload`,避免影响外链图片转存。 --- # 7. 修改 Vditor 上传字段名 打开: ```text /usr/plugins/Handsome/assets/js/origin/editor-js.php ``` 找到 Vditor 的上传配置: ```js upload: { url: uploadURL.replace('CID', $('input[name="cid"]').val()), ``` 改成: ```js upload: { url: uploadURL.replace('CID', $('input[name="cid"]').val()), fieldName: 'file', ``` 说明: Typecho 1.3 原生上传接口期望的字段名是:`file`,而不是:`file[]` --- # 8. 补充:处理 Typecho 返回 `false` 时的错误提示 如果复制或粘贴了 Typecho 后台不允许上传的文件格式,例如后台没有放行的图片类型、文档、动图或其他文件,Typecho 原生上传接口可能会直接返回: ```json false ``` 这类返回没有具体错误信息,Vditor 只会认为上传失败。为了让后台提示更明确,可以在 Vditor 的 `format` 里单独判断 `false`,并显示“文件格式不支持或上传被服务器拒绝”。 继续在: ```text /usr/plugins/Handsome/assets/js/origin/editor-js.php ``` 找到: ```php format: function (files, responseText) { // console.log(responseText); var data = JSON.parse(responseText); var ret = {}; const file = {}; file.id = Math.floor(Math.random() * 1000) + ""; if (data && data[1] && data[1].url) { file.name = data[1].title; file.image = data[1].isImage; file.url = data[1].url; fileUploadStart(file); fileUploadComplete(file.id, data[1].url, data[1]); ret.msg = ""; ret.code = 0; ret.data = {}; ret.data.errFiles = []; var fileName = data[1].title; var filePath = data[1].url; // console.log("data[0]_ok,data[1].url,data[1].title" + data[1].url + "|" + data[1].title); var map = {}; map[fileName] = filePath; ret.data.succMap = map; // console.log(ret.data.succMap); } else { //处理上传错误 const error = {}; error.file = file; //todo 拿不到文件名称 file.name = "文件上传失败"; error.code = 404; fileUploadStart(file); fileUploadError(error); console.log("no!!"); ret.msg = ""; ret.code = 0; ret.data = {}; ret.data.errFiles = []; var fileName = ""; var filePath = ""; ret.data.succMap = {}; } // console.log(JSON.stringify(ret)); return JSON.stringify(ret); } ``` 替换为: ```js format: function (files, responseText) { var ret = { msg: '', code: 1, data: { errFiles: [], succMap: {} } }; var uploadFile = files && files.length ? files[0] : {}; var file = { id: Math.floor(Math.random() * 100000) + '', name: uploadFile.name || '文件上传失败' }; // Typecho 原生上传失败时可能直接返回 false // 常见原因:文件格式不支持、附件类型未放行、上传目录不可写、文件大小超限等 if (responseText === false || responseText === 'false') { fileUploadStart(file); fileUploadError({ file: file, type: 'type', message: '文件上传失败:文件格式不支持或上传被服务器拒绝' }); ret.msg = '文件上传失败:文件格式不支持或上传被服务器拒绝'; ret.data.errFiles = [file.name]; return JSON.stringify(ret); } var data; try { data = JSON.parse(responseText); } catch (e) { fileUploadStart(file); fileUploadError({ file: file, type: 'network', message: '文件上传失败:服务器返回内容异常' }); ret.msg = '文件上传失败:服务器返回内容异常'; ret.data.errFiles = [file.name]; return JSON.stringify(ret); } // Typecho 原生上传成功时,通常返回 [url, attachment] if (data && data[0] && data[1]) { file.name = data[1].title || file.name; file.image = data[1].isImage; file.url = data[1].url || data[0]; fileUploadStart(file); fileUploadComplete(file.id, file.url, data[1]); ret.msg = ''; ret.code = 0; ret.data.errFiles = []; ret.data.succMap[file.name] = file.url; return JSON.stringify(ret); } // 兼容 JSON 解析成功但内容不是预期结构的情况 fileUploadStart(file); fileUploadError({ file: file, type: 'network', message: '文件上传失败:上传接口返回格式异常' }); ret.msg = '文件上传失败:上传接口返回格式异常'; ret.data.errFiles = [file.name]; return JSON.stringify(ret); } ``` 这样当用户粘贴了不支持的文件格式时,提示会更明确: ```text 文件上传失败:文件格式不支持或上传被服务器拒绝 ``` 注意:`false` 不一定只代表格式不支持,也可能是下面这些原因: ```text /usr/uploads 不可写 /usr/uploads/年份/月 无法创建 后台允许上传的附件类型不包含当前文件扩展名 PHP upload_max_filesize / post_max_size 限制过小 上传请求安全校验失败 文件名或 MIME 类型异常 ``` 如果只是在复制不支持的格式时出现 `false`,优先检查 Typecho 后台的“允许上传的文件类型”。 Loading... # 1. 问题现象 Typecho 升级到 `1.3` 后,Handsome 后台编辑器切换为 `Vditor`,直接复制或粘贴图片时上传失败。 常见报错: ```text write-post.php:2108 Uncaught ReferenceError: plupload is not defined ``` 浏览器 Network 中可能看到: ```text /action/multi-upload?do=uploadfile... ``` 接口返回: ```json { "msg": "图片外链格式不正确", "code": 100, "data": { "url": "", "title": "", "isImage": false } } ``` --- # 2. 问题原因 Handsome 的 Vditor 上传逻辑仍然兼容旧版 Typecho 后台上传方式。 Typecho 1.3 后: - 后台上传不再依赖 `plupload`; - 原生上传接口是 `/action/upload`; - 官方上传逻辑使用 `fetch + FormData`; - Typecho 原生上传字段名是 `file`; - Handsome 的 `/action/multi-upload?do=uploadfile` 更偏向处理外链图片; - Vditor 粘贴图片上传的是二进制文件,不是外链 URL。 所以会出现两个问题: 1. 上传失败时,旧代码引用 `plupload`,导致 JS 报错; 2. Vditor 的二进制图片被 Handsome 的 `multi-upload` 当成外链图片处理,返回“图片外链格式不正确”。 --- # 3. 推荐修复方案 这里推荐使用: ```text 保留 Handsome 的 multi-upload,但在收到二进制文件时转交 Typecho 原生上传 ``` 也就是: ```text Vditor 粘贴图片 / 拖拽上传文件 => /action/multi-upload?do=uploadfile => Action.php 检测到 $_FILES => 转交 Typecho 原生 Widget_Upload => 返回 Typecho 原生上传结果 ``` 外链图片转存仍然保留 Handsome 原逻辑: ```text 外链图片转存 => /action/multi-upload?do=uploadfile => 没有 $_FILES => 继续走 Handsome_Upload => 返回 Handsome 的 {code, msg, data} ``` 这样既修复 Vditor 粘贴上传,又不会破坏 Handsome 原来的外链图片处理能力。 --- # 4. 需要修改的文件 主要修改两个文件: ```text /usr/plugins/Handsome/Action.php /usr/plugins/Handsome/assets/js/origin/editor-js.php ``` 修改前务必备份。 --- # 5. 修复 `plupload is not defined` 继续在: ```text /usr/plugins/Handsome/assets/js/origin/editor-js.php ``` 找到类似下面的函数: ```js function fileUploadError(error) { var file = error.file, code = error.code, word; switch (code) { case plupload.FILE_SIZE_ERROR: word = '文件大小超过限制'; break; case plupload.FILE_EXTENSION_ERROR: word = '文件扩展名不被支持'; break; case plupload.FILE_DUPLICATE_ERROR: word = '文件已经上传过'; break; case plupload.HTTP_ERROR: default: word = '上传出现错误'; break; } var fileError = '%s 上传失败'.replace('%s', file.name), li, exist = $('#' + file.id); if (exist.length > 0) { li = exist.removeClass('loading').html(fileError); } else { li = $('<li>' + fileError + '<br />' + word + '</li>').appendTo('#file-list'); } li.effect('highlight', {color: '#FBC2C4'}, 2000, function () { $(this).remove(); }); try { this.removeFile(file); } catch (e) { } } ``` 替换为: ```js function fileUploadError(error) { var file = error.file || {}; var code = error.code || error.type || ''; var word = '上传出现错误'; switch (code) { case 'size': case 'FILE_SIZE_ERROR': case -600: word = '文件大小超过限制'; break; case 'type': case 'FILE_EXTENSION_ERROR': case -601: word = '文件扩展名不被支持'; break; case 'duplicate': case 'FILE_DUPLICATE_ERROR': case -602: word = '文件已经上传过'; break; case 'network': case 'HTTP_ERROR': case -200: default: word = '上传出现错误'; break; } // 允许 format() 传入更具体的错误提示,例如 Typecho 返回 false 时的格式不支持提示 if (error.message) { word = error.message; } file.id = file.id || ('upload-error-' + Date.now()); file.name = file.name || '文件'; var fileError = '%s 上传失败'.replace('%s', file.name), li, exist = $('#' + file.id); if (exist.length > 0) { li = exist.removeClass('loading').html(fileError + '<br />' + word); } else { li = $('<li>' + fileError + '<br />' + word + '</li>').appendTo('#file-list'); } li.effect('highlight', {color: '#FBC2C4'}, 2000, function () { $(this).remove(); }); } ``` --- # 6. 修改 `Action.php`:让 multi-upload 支持 Vditor 二进制上传 打开: ```text /usr/plugins/Handsome/Action.php ``` 找到: ```php public function uploadApi() { if (class_exists("Handsome_Upload")) { $reflectionWidget = new ReflectionClass("Handsome_Upload"); if ($reflectionWidget->implementsInterface('Widget_Interface_Do')) { $this->widget("Handsome_Upload")->action(); } } } ``` 替换为: ```php public function uploadApi() { // Vditor 粘贴/拖拽上传的是二进制文件,交给 Typecho 原生上传 if (!empty($_FILES)) { $this->widget('Widget_Upload')->action(); return; } // 没有 $_FILES 的情况,保留 Handsome 原来的外链图片转存逻辑 if (class_exists("Handsome_Upload")) { $reflectionWidget = new ReflectionClass("Handsome_Upload"); if ($reflectionWidget->implementsInterface('Widget_Interface_Do')) { $this->widget("Handsome_Upload")->action(); } } } ``` 说明: - Vditor 粘贴图片时提交的是 `multipart/form-data`,文件会进入 `$_FILES`; - 检测到 `$_FILES` 后,直接交给 Typecho 原生 `Widget_Upload` 处理; - 没有 `$_FILES` 时,继续走 Handsome 原来的 `Handsome_Upload`,避免影响外链图片转存。 --- # 7. 修改 Vditor 上传字段名 打开: ```text /usr/plugins/Handsome/assets/js/origin/editor-js.php ``` 找到 Vditor 的上传配置: ```js upload: { url: uploadURL.replace('CID', $('input[name="cid"]').val()), ``` 改成: ```js upload: { url: uploadURL.replace('CID', $('input[name="cid"]').val()), fieldName: 'file', ``` 说明: Typecho 1.3 原生上传接口期望的字段名是:`file`,而不是:`file[]` --- # 8. 补充:处理 Typecho 返回 `false` 时的错误提示 如果复制或粘贴了 Typecho 后台不允许上传的文件格式,例如后台没有放行的图片类型、文档、动图或其他文件,Typecho 原生上传接口可能会直接返回: ```json false ``` 这类返回没有具体错误信息,Vditor 只会认为上传失败。为了让后台提示更明确,可以在 Vditor 的 `format` 里单独判断 `false`,并显示“文件格式不支持或上传被服务器拒绝”。 继续在: ```text /usr/plugins/Handsome/assets/js/origin/editor-js.php ``` 找到: ```php format: function (files, responseText) { // console.log(responseText); var data = JSON.parse(responseText); var ret = {}; const file = {}; file.id = Math.floor(Math.random() * 1000) + ""; if (data && data[1] && data[1].url) { file.name = data[1].title; file.image = data[1].isImage; file.url = data[1].url; fileUploadStart(file); fileUploadComplete(file.id, data[1].url, data[1]); ret.msg = ""; ret.code = 0; ret.data = {}; ret.data.errFiles = []; var fileName = data[1].title; var filePath = data[1].url; // console.log("data[0]_ok,data[1].url,data[1].title" + data[1].url + "|" + data[1].title); var map = {}; map[fileName] = filePath; ret.data.succMap = map; // console.log(ret.data.succMap); } else { //处理上传错误 const error = {}; error.file = file; //todo 拿不到文件名称 file.name = "文件上传失败"; error.code = 404; fileUploadStart(file); fileUploadError(error); console.log("no!!"); ret.msg = ""; ret.code = 0; ret.data = {}; ret.data.errFiles = []; var fileName = ""; var filePath = ""; ret.data.succMap = {}; } // console.log(JSON.stringify(ret)); return JSON.stringify(ret); } ``` 替换为: ```js format: function (files, responseText) { var ret = { msg: '', code: 1, data: { errFiles: [], succMap: {} } }; var uploadFile = files && files.length ? files[0] : {}; var file = { id: Math.floor(Math.random() * 100000) + '', name: uploadFile.name || '文件上传失败' }; // Typecho 原生上传失败时可能直接返回 false // 常见原因:文件格式不支持、附件类型未放行、上传目录不可写、文件大小超限等 if (responseText === false || responseText === 'false') { fileUploadStart(file); fileUploadError({ file: file, type: 'type', message: '文件上传失败:文件格式不支持或上传被服务器拒绝' }); ret.msg = '文件上传失败:文件格式不支持或上传被服务器拒绝'; ret.data.errFiles = [file.name]; return JSON.stringify(ret); } var data; try { data = JSON.parse(responseText); } catch (e) { fileUploadStart(file); fileUploadError({ file: file, type: 'network', message: '文件上传失败:服务器返回内容异常' }); ret.msg = '文件上传失败:服务器返回内容异常'; ret.data.errFiles = [file.name]; return JSON.stringify(ret); } // Typecho 原生上传成功时,通常返回 [url, attachment] if (data && data[0] && data[1]) { file.name = data[1].title || file.name; file.image = data[1].isImage; file.url = data[1].url || data[0]; fileUploadStart(file); fileUploadComplete(file.id, file.url, data[1]); ret.msg = ''; ret.code = 0; ret.data.errFiles = []; ret.data.succMap[file.name] = file.url; return JSON.stringify(ret); } // 兼容 JSON 解析成功但内容不是预期结构的情况 fileUploadStart(file); fileUploadError({ file: file, type: 'network', message: '文件上传失败:上传接口返回格式异常' }); ret.msg = '文件上传失败:上传接口返回格式异常'; ret.data.errFiles = [file.name]; return JSON.stringify(ret); } ``` 这样当用户粘贴了不支持的文件格式时,提示会更明确: ```text 文件上传失败:文件格式不支持或上传被服务器拒绝 ``` 注意:`false` 不一定只代表格式不支持,也可能是下面这些原因: ```text /usr/uploads 不可写 /usr/uploads/年份/月 无法创建 后台允许上传的附件类型不包含当前文件扩展名 PHP upload_max_filesize / post_max_size 限制过小 上传请求安全校验失败 文件名或 MIME 类型异常 ``` 如果只是在复制不支持的格式时出现 `false`,优先检查 Typecho 后台的“允许上传的文件类型”。 Last modification:May 17, 2026 © Allow specification reprint Support Appreciate the author AliPayWeChat Like 1 喜欢我的文章吗? 别忘了点赞或赞赏,让我知道创作的路上有你陪伴。