feat: 简化vite配置并修复去背景接口

- 简化vite.config.ts,删除冗余的apps.txt读取和文件服务插件
- 更新去背景接口路径为 /tools/rmbg/file/free
- 修复流式响应处理:使用fetch API正确处理NDJSON格式
- 添加/tools代理配置以支持新的接口路径
- 优化错误处理,适配fetch API的错误格式
This commit is contained in:
jingrow 2026-01-02 20:00:53 +08:00
parent 328a9f4408
commit 6b638ac143
3 changed files with 197 additions and 139 deletions

View File

@ -521,28 +521,51 @@ const handleRemoveBackground = async () => {
const formData = new FormData() const formData = new FormData()
formData.append('file', uploadedImage.value) formData.append('file', uploadedImage.value)
// 使 file/free API // 使 fetch NDJSON
// Content-Type boundary const controller = new AbortController()
const response = await axios.post( const timeoutId = setTimeout(() => controller.abort(), 180000)
'/jingrow.tools.remove_background.remove_background.remove_background_from_file_free',
formData, const response = await fetch('/tools/rmbg/file/free', {
{ method: 'POST',
timeout: 180000 body: formData,
signal: controller.signal
})
clearTimeout(timeoutId)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
} }
)
if (response.data?.success && response.data?.data) { // NDJSON JSON
const result = response.data.data const reader = response.body?.getReader()
const decoder = new TextDecoder()
let buffer = ''
// remove_background_from_file if (!reader) {
// result successdata throw new Error('Response body is not readable')
if (result.success && result.data && Array.isArray(result.data) && result.data.length > 0) { }
const firstResult = result.data[0]
if (firstResult.success && firstResult.image_url) { while (true) {
resultImage.value = firstResult.image_url 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 // blob URL
await cacheResultImage(firstResult.image_url) await cacheResultImage(result.image_url)
// //
if (currentHistoryIndex.value === -1 && uploadedImage.value && uploadedImageUrl.value) { if (currentHistoryIndex.value === -1 && uploadedImage.value && uploadedImageUrl.value) {
@ -550,30 +573,66 @@ const handleRemoveBackground = async () => {
id: `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, id: `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
originalImageUrl: uploadedImageUrl.value, originalImageUrl: uploadedImageUrl.value,
originalImageFile: uploadedImage.value, originalImageFile: uploadedImage.value,
resultImage: firstResult.image_url, resultImage: result.image_url,
timestamp: Date.now() timestamp: Date.now()
} }
historyList.value.unshift(historyItem) historyList.value.unshift(historyItem)
currentHistoryIndex.value = 0 currentHistoryIndex.value = 0
} else if (currentHistoryIndex.value >= 0) { } else if (currentHistoryIndex.value >= 0) {
historyList.value[currentHistoryIndex.value].resultImage = firstResult.image_url historyList.value[currentHistoryIndex.value].resultImage = result.image_url
}
} else {
message.error(firstResult.error || t('Failed to remove background'))
} }
return // 退
} else { } else {
message.error(result.error || t('Failed to remove background')) 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)
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 { } else {
message.error(response.data?.error || response.data?.message || t('Failed to remove background')) 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(t('No image data returned'))
} }
} catch (error: any) { } catch (error: any) {
let errorMessage = t('Failed to remove background') 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) { } else if (error.message) {
errorMessage = error.message errorMessage = error.message
} }
message.error(errorMessage) message.error(errorMessage)
} finally { } finally {
processing.value = false processing.value = false

View File

@ -612,71 +612,115 @@ const handleRemoveBackground = async () => {
const formData = new FormData() const formData = new FormData()
formData.append('file', uploadedImage.value) formData.append('file', uploadedImage.value)
const response = await axios.post( // 使 fetch NDJSON
'/jingrow.tools.remove_background.remove_background.remove_background_from_file_free', const controller = new AbortController()
formData, const timeoutId = setTimeout(() => controller.abort(), 180000)
{
timeout: 180000 const response = await fetch('/tools/rmbg/file/free', {
method: 'POST',
body: formData,
signal: controller.signal
})
clearTimeout(timeoutId)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
} }
)
if (response.data?.success && response.data?.data) { // NDJSON JSON
const result = response.data.data const reader = response.body?.getReader()
const decoder = new TextDecoder()
let buffer = ''
if (result.success) { if (!reader) {
if (result.data && Array.isArray(result.data) && result.data.length > 0) { throw new Error('Response body is not readable')
const firstResult = result.data[0] }
if (firstResult.success && firstResult.image_url) { while (true) {
resultImage.value = firstResult.image_url 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 // blob URL
await cacheResultImage(firstResult.image_url) await cacheResultImage(result.image_url)
if (currentHistoryIndex.value === -1 && uploadedImage.value && uploadedImageUrl.value) { if (currentHistoryIndex.value === -1 && uploadedImage.value && uploadedImageUrl.value) {
const historyItem: HistoryItem = { const historyItem: HistoryItem = {
id: `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, id: `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
originalImageUrl: uploadedImageUrl.value, originalImageUrl: uploadedImageUrl.value,
originalImageFile: uploadedImage.value, originalImageFile: uploadedImage.value,
resultImage: firstResult.image_url, resultImage: result.image_url,
timestamp: Date.now() timestamp: Date.now()
} }
historyList.value.unshift(historyItem) historyList.value.unshift(historyItem)
currentHistoryIndex.value = 0 currentHistoryIndex.value = 0
} else if (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 { } else {
message.error(firstResult.error || t('Failed to remove background')) 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)
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 { } else {
message.error(t('No image data returned')) message.error(t('No image data returned'))
} }
} else {
message.error(result.error || t('Failed to remove background'))
}
} else {
message.error(response.data?.error || response.data?.message || t('Failed to remove background'))
}
} catch (error: any) { } catch (error: any) {
let errorMessage = t('Failed to remove background') let errorMessage = t('Failed to remove background')
if (error.response) { if (error.name === 'AbortError') {
if (error.response.data) { errorMessage = t('Request timeout. Please try again.')
errorMessage = error.response.data.error || } else if (error.message) {
error.response.data.detail || errorMessage = error.message
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
} }
message.error(errorMessage) message.error(errorMessage)

View File

@ -1,58 +1,23 @@
import { defineConfig, loadEnv } from 'vite' import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
import Icons from 'unplugin-icons/vite' import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver' import IconsResolver from 'unplugin-icons/resolver'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs'
import { createRequire } from 'node:module'
import { vitePluginPrerender } from './scripts/vite-plugin-prerender.js' 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 // 计算本工程中的 apps 目录(当前文件位于 apps/jingrow/frontend/vite.config.ts
const currentDir = fileURLToPath(new URL('.', import.meta.url)) const currentDir = fileURLToPath(new URL('.', import.meta.url))
const appsDir = path.resolve(currentDir, '..', '..') const appsDir = path.resolve(currentDir, '..', '..')
const APPS_ORDER = loadAppsOrder(appsDir)
export default defineConfig(({ mode, command }) => { export default defineConfig(({ mode, command }) => {
const env = loadEnv(mode, process.cwd(), '') 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_HOST = env.VITE_FRONTEND_HOST || '0.0.0.0'
const FRONTEND_PORT = Number(env.VITE_FRONTEND_PORT) || 3100 const FRONTEND_PORT = Number(env.VITE_FRONTEND_PORT) || 3100
const ALLOWED_HOSTS = (env.VITE_ALLOWED_HOSTS || '').split(',').map((s) => s.trim()).filter(Boolean) 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 { return {
plugins: [ plugins: [
vue(), 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({ command === 'build' && vitePluginPrerender({
// 路由列表会自动从工具 store 生成 // 路由列表会自动从工具 store 生成
@ -108,21 +64,20 @@ export default defineConfig(({ mode, command }) => {
allow: [appsDir] allow: [appsDir]
}, },
proxy: { proxy: {
'/api/data': { '/api': {
target: BACKEND_URL, target: BACKEND_URL,
changeOrigin: true, changeOrigin: true,
secure: false, secure: true,
cookieDomainRewrite: { '*': '' },
cookiePathRewrite: { '*': '/' },
configure: cookieRewriteConfigure
}, },
'/jingrow': { '/jingrow': {
target: BACKEND_URL, target: BACKEND_URL,
changeOrigin: true, changeOrigin: true,
secure: false, secure: true,
cookieDomainRewrite: { '*': '' }, },
cookiePathRewrite: { '*': '/' }, '/tools': {
configure: cookieRewriteConfigure target: BACKEND_URL,
changeOrigin: true,
secure: true,
} }
} }
}, },
@ -135,8 +90,8 @@ export default defineConfig(({ mode, command }) => {
define: { define: {
// 确保环境变量在构建时可用 // 确保环境变量在构建时可用
__APP_VERSION__: JSON.stringify(process.env.npm_package_version), __APP_VERSION__: JSON.stringify(process.env.npm_package_version),
// 注入 apps.txt 的应用顺序到前端 // 应用顺序(已移除 apps.txt 支持,使用默认值)
__APPS_ORDER__: JSON.stringify(APPS_ORDER) __APPS_ORDER__: JSON.stringify(['jingrow'])
} }
} }
}) })