From 6b638ac1435676e3dbc579933bcf26470f1bca1f Mon Sep 17 00:00:00 2001 From: jingrow Date: Fri, 2 Jan 2026 20:00:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AE=80=E5=8C=96vite=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=B9=B6=E4=BF=AE=E5=A4=8D=E5=8E=BB=E8=83=8C=E6=99=AF?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 简化vite.config.ts,删除冗余的apps.txt读取和文件服务插件 - 更新去背景接口路径为 /tools/rmbg/file/free - 修复流式响应处理:使用fetch API正确处理NDJSON格式 - 添加/tools代理配置以支持新的接口路径 - 优化错误处理,适配fetch API的错误格式 --- src/views/HomePage.vue | 119 ++++++++++---- .../remove_background/remove_background.vue | 150 +++++++++++------- vite.config.ts | 67 ++------ 3 files changed, 197 insertions(+), 139 deletions(-) diff --git a/src/views/HomePage.vue b/src/views/HomePage.vue index 0121455..95e4c6c 100644 --- a/src/views/HomePage.vue +++ b/src/views/HomePage.vue @@ -521,59 +521,118 @@ const handleRemoveBackground = async () => { const formData = new FormData() formData.append('file', uploadedImage.value) - // 使用 file/free API,无需认证 - // 注意:不要手动设置 Content-Type,让浏览器自动设置(包含 boundary) - const response = await axios.post( - '/jingrow.tools.remove_background.remove_background.remove_background_from_file_free', - formData, - { - timeout: 180000 - } - ) + // 使用 fetch 处理流式响应(NDJSON 格式) + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 180000) + + const response = await fetch('/tools/rmbg/file/free', { + method: 'POST', + body: formData, + signal: controller.signal + }) + + clearTimeout(timeoutId) - if (response.data?.success && response.data?.data) { - const result = response.data.data - - // 处理返回格式(与 remove_background_from_file 一致) - // result 是函数返回的字典,包含 success、data 数组等字段 - if (result.success && result.data && Array.isArray(result.data) && result.data.length > 0) { - const firstResult = result.data[0] - if (firstResult.success && firstResult.image_url) { - resultImage.value = firstResult.image_url + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + // 读取流式响应(NDJSON 格式,每行一个 JSON 对象) + const reader = response.body?.getReader() + const decoder = new TextDecoder() + let buffer = '' + + if (!reader) { + throw new Error('Response body is not readable') + } + + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + buffer = lines.pop() || '' // 保留最后一个不完整的行 + + // 处理完整的行 + for (const line of lines) { + if (line.trim()) { + try { + const result = JSON.parse(line.trim()) + + // 后端返回格式:{"status": "success", "image_url": "..."} + if (result.status === 'success' && result.image_url) { + resultImage.value = result.image_url + + // 缓存图片 blob URL 用于下载 + await cacheResultImage(result.image_url) + + // 添加到历史记录 + if (currentHistoryIndex.value === -1 && uploadedImage.value && uploadedImageUrl.value) { + const historyItem: HistoryItem = { + id: `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + originalImageUrl: uploadedImageUrl.value, + originalImageFile: uploadedImage.value, + resultImage: result.image_url, + timestamp: Date.now() + } + historyList.value.unshift(historyItem) + currentHistoryIndex.value = 0 + } else if (currentHistoryIndex.value >= 0) { + historyList.value[currentHistoryIndex.value].resultImage = result.image_url + } + return // 成功处理,退出 + } else { + message.error(result.error || t('Failed to remove background')) + } + } catch (parseError) { + console.error('Failed to parse JSON:', parseError, 'Line:', line) + } + } + } + } + + // 处理最后一行 + if (buffer.trim()) { + try { + const result = JSON.parse(buffer.trim()) + if (result.status === 'success' && result.image_url) { + resultImage.value = result.image_url + await cacheResultImage(result.image_url) - // 缓存图片 blob URL 用于下载 - await cacheResultImage(firstResult.image_url) - - // 添加到历史记录 if (currentHistoryIndex.value === -1 && uploadedImage.value && uploadedImageUrl.value) { const historyItem: HistoryItem = { id: `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, originalImageUrl: uploadedImageUrl.value, originalImageFile: uploadedImage.value, - resultImage: firstResult.image_url, + resultImage: result.image_url, timestamp: Date.now() } historyList.value.unshift(historyItem) currentHistoryIndex.value = 0 } else if (currentHistoryIndex.value >= 0) { - historyList.value[currentHistoryIndex.value].resultImage = firstResult.image_url + historyList.value[currentHistoryIndex.value].resultImage = result.image_url } + return } else { - message.error(firstResult.error || t('Failed to remove background')) + message.error(result.error || t('Failed to remove background')) } - } else { - message.error(result.error || t('Failed to remove background')) + } catch (parseError) { + console.error('Failed to parse final JSON:', parseError) + message.error(t('Failed to parse response')) } } else { - message.error(response.data?.error || response.data?.message || t('Failed to remove background')) + message.error(t('No image data returned')) } } catch (error: any) { let errorMessage = t('Failed to remove background') - if (error.response?.data?.error) { - errorMessage = error.response.data.error + + if (error.name === 'AbortError') { + errorMessage = t('Request timeout. Please try again.') } else if (error.message) { errorMessage = error.message } + message.error(errorMessage) } finally { processing.value = false diff --git a/src/views/tools/remove_background/remove_background.vue b/src/views/tools/remove_background/remove_background.vue index 01616ca..004a155 100644 --- a/src/views/tools/remove_background/remove_background.vue +++ b/src/views/tools/remove_background/remove_background.vue @@ -612,71 +612,115 @@ const handleRemoveBackground = async () => { const formData = new FormData() formData.append('file', uploadedImage.value) - const response = await axios.post( - '/jingrow.tools.remove_background.remove_background.remove_background_from_file_free', - formData, - { - timeout: 180000 - } - ) + // 使用 fetch 处理流式响应(NDJSON 格式) + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 180000) + + const response = await fetch('/tools/rmbg/file/free', { + method: 'POST', + body: formData, + signal: controller.signal + }) + + clearTimeout(timeoutId) - if (response.data?.success && response.data?.data) { - const result = response.data.data - - if (result.success) { - if (result.data && Array.isArray(result.data) && result.data.length > 0) { - const firstResult = result.data[0] - - if (firstResult.success && firstResult.image_url) { - resultImage.value = firstResult.image_url + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + // 读取流式响应(NDJSON 格式,每行一个 JSON 对象) + const reader = response.body?.getReader() + const decoder = new TextDecoder() + let buffer = '' + + if (!reader) { + throw new Error('Response body is not readable') + } + + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + buffer = lines.pop() || '' // 保留最后一个不完整的行 + + // 处理完整的行 + for (const line of lines) { + if (line.trim()) { + try { + const result = JSON.parse(line.trim()) - // 缓存图片 blob URL 用于下载 - await cacheResultImage(firstResult.image_url) - - if (currentHistoryIndex.value === -1 && uploadedImage.value && uploadedImageUrl.value) { - const historyItem: HistoryItem = { - id: `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - originalImageUrl: uploadedImageUrl.value, - originalImageFile: uploadedImage.value, - resultImage: firstResult.image_url, - timestamp: Date.now() + // 后端返回格式:{"status": "success", "image_url": "..."} + if (result.status === 'success' && result.image_url) { + resultImage.value = result.image_url + + // 缓存图片 blob URL 用于下载 + await cacheResultImage(result.image_url) + + if (currentHistoryIndex.value === -1 && uploadedImage.value && uploadedImageUrl.value) { + const historyItem: HistoryItem = { + id: `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + originalImageUrl: uploadedImageUrl.value, + originalImageFile: uploadedImage.value, + resultImage: result.image_url, + timestamp: Date.now() + } + historyList.value.unshift(historyItem) + currentHistoryIndex.value = 0 + } else if (currentHistoryIndex.value >= 0) { + historyList.value[currentHistoryIndex.value].resultImage = result.image_url } - historyList.value.unshift(historyItem) - currentHistoryIndex.value = 0 - } else if (currentHistoryIndex.value >= 0) { - historyList.value[currentHistoryIndex.value].resultImage = firstResult.image_url + return // 成功处理,退出 + } else { + message.error(result.error || t('Failed to remove background')) } - } else { - message.error(firstResult.error || t('Failed to remove background')) + } catch (parseError) { + console.error('Failed to parse JSON:', parseError, 'Line:', line) } - } else { - message.error(t('No image data returned')) } - } else { - message.error(result.error || t('Failed to remove background')) + } + } + + // 处理最后一行 + if (buffer.trim()) { + try { + const result = JSON.parse(buffer.trim()) + if (result.status === 'success' && result.image_url) { + resultImage.value = result.image_url + await cacheResultImage(result.image_url) + + if (currentHistoryIndex.value === -1 && uploadedImage.value && uploadedImageUrl.value) { + const historyItem: HistoryItem = { + id: `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + originalImageUrl: uploadedImageUrl.value, + originalImageFile: uploadedImage.value, + resultImage: result.image_url, + timestamp: Date.now() + } + historyList.value.unshift(historyItem) + currentHistoryIndex.value = 0 + } else if (currentHistoryIndex.value >= 0) { + historyList.value[currentHistoryIndex.value].resultImage = result.image_url + } + return + } else { + message.error(result.error || t('Failed to remove background')) + } + } catch (parseError) { + console.error('Failed to parse final JSON:', parseError) + message.error(t('Failed to parse response')) } } else { - message.error(response.data?.error || response.data?.message || t('Failed to remove background')) + message.error(t('No image data returned')) } } catch (error: any) { let errorMessage = t('Failed to remove background') - if (error.response) { - if (error.response.data) { - errorMessage = error.response.data.error || - error.response.data.detail || - error.response.data.message || - errorMessage - } - if (error.response.status === 404) { - errorMessage = t('API endpoint not found. Please check if the backend service is running.') - } else if (error.response.status === 401 || error.response.status === 403) { - errorMessage = t('Authentication failed. Please login again.') - } - } else if (error.request) { - errorMessage = t('Network error. Please check your connection.') - } else { - errorMessage = error.message || errorMessage + if (error.name === 'AbortError') { + errorMessage = t('Request timeout. Please try again.') + } else if (error.message) { + errorMessage = error.message } message.error(errorMessage) diff --git a/vite.config.ts b/vite.config.ts index c67f35b..6f6b2c8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,58 +1,23 @@ import { defineConfig, loadEnv } from 'vite' import vue from '@vitejs/plugin-vue' -import { resolve } from 'path' import { fileURLToPath, URL } from 'node:url' import Icons from 'unplugin-icons/vite' import IconsResolver from 'unplugin-icons/resolver' import Components from 'unplugin-vue-components/vite' import path from 'node:path' -import fs from 'node:fs' -import { createRequire } from 'node:module' import { vitePluginPrerender } from './scripts/vite-plugin-prerender.js' -// 统一处理后端 Set-Cookie,移除 Secure 标志,便于在 HTTP 开发环境保存 Cookie -const cookieRewriteConfigure = (proxy: any) => { - proxy.on('proxyRes', (proxyRes: any) => { - const setCookieHeaders = proxyRes.headers['set-cookie'] - if (setCookieHeaders) { - proxyRes.headers['set-cookie'] = setCookieHeaders.map((cookie: string) => { - return cookie - .replace(/;\s*[Ss]ecure/gi, '') - .replace(/,\s*[Ss]ecure/gi, '') - }) - } - }) -} - -// 读取 apps.txt 确定应用优先级(靠后优先) -function loadAppsOrder(appsDir: string) { - const appsTxt = path.join(appsDir, 'apps.txt') - try { - const content = fs.readFileSync(appsTxt, 'utf-8') - return content - .split(/\r?\n/) - .map((s) => s.trim()) - .filter(Boolean) - } catch { - return ['jingrow'] - } -} - // 计算本工程中的 apps 目录(当前文件位于 apps/jingrow/frontend/vite.config.ts) const currentDir = fileURLToPath(new URL('.', import.meta.url)) const appsDir = path.resolve(currentDir, '..', '..') -const APPS_ORDER = loadAppsOrder(appsDir) export default defineConfig(({ mode, command }) => { const env = loadEnv(mode, process.cwd(), '') - const BACKEND_URL = env.VITE_BACKEND_SERVER_URL || 'http://localhost:9001' + const BACKEND_URL = env.VITE_BACKEND_SERVER_URL || 'https://api.jingrow.com' const FRONTEND_HOST = env.VITE_FRONTEND_HOST || '0.0.0.0' const FRONTEND_PORT = Number(env.VITE_FRONTEND_PORT) || 3100 const ALLOWED_HOSTS = (env.VITE_ALLOWED_HOSTS || '').split(',').map((s) => s.trim()).filter(Boolean) - // 开发环境:文件目录路径 - const filesDir = command === 'serve' ? path.resolve(currentDir, '..', 'jingrow', 'public', 'files') : null - return { plugins: [ vue(), @@ -68,15 +33,6 @@ export default defineConfig(({ mode, command }) => { }), ], }), - // 开发环境:直接服务文件系统 - command === 'serve' && filesDir && { - name: 'serve-files', - configureServer(server) { - const require = createRequire(import.meta.url) - const serve = require('serve-static')(filesDir, { index: false }) - server.middlewares.use('/files', serve) - } - }, // 预渲染插件(仅在构建时启用) command === 'build' && vitePluginPrerender({ // 路由列表会自动从工具 store 生成 @@ -108,21 +64,20 @@ export default defineConfig(({ mode, command }) => { allow: [appsDir] }, proxy: { - '/api/data': { + '/api': { target: BACKEND_URL, changeOrigin: true, - secure: false, - cookieDomainRewrite: { '*': '' }, - cookiePathRewrite: { '*': '/' }, - configure: cookieRewriteConfigure + secure: true, }, '/jingrow': { target: BACKEND_URL, changeOrigin: true, - secure: false, - cookieDomainRewrite: { '*': '' }, - cookiePathRewrite: { '*': '/' }, - configure: cookieRewriteConfigure + secure: true, + }, + '/tools': { + target: BACKEND_URL, + changeOrigin: true, + secure: true, } } }, @@ -135,8 +90,8 @@ export default defineConfig(({ mode, command }) => { define: { // 确保环境变量在构建时可用 __APP_VERSION__: JSON.stringify(process.env.npm_package_version), - // 注入 apps.txt 的应用顺序到前端 - __APPS_ORDER__: JSON.stringify(APPS_ORDER) + // 应用顺序(已移除 apps.txt 支持,使用默认值) + __APPS_ORDER__: JSON.stringify(['jingrow']) } } })