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()
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.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
)
if (response.data?.success && response.data?.data) {
const result = response.data.data
// NDJSON JSON
const reader = response.body?.getReader()
const decoder = new TextDecoder()
let buffer = ''
// remove_background_from_file
// result successdata
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 (!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(firstResult.image_url)
await cacheResultImage(result.image_url)
//
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)}`,
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
}
} else {
message.error(firstResult.error || t('Failed to remove background'))
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)
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(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) {
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

View File

@ -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.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
)
if (response.data?.success && response.data?.data) {
const result = response.data.data
// NDJSON JSON
const reader = response.body?.getReader()
const decoder = new TextDecoder()
let buffer = ''
if (result.success) {
if (result.data && Array.isArray(result.data) && result.data.length > 0) {
const firstResult = result.data[0]
if (!reader) {
throw new Error('Response body is not readable')
}
if (firstResult.success && firstResult.image_url) {
resultImage.value = firstResult.image_url
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(firstResult.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: 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'))
}
} 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 {
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) {
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)

View File

@ -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'])
}
}
})