diff --git a/app/composables/useApi.js b/app/composables/useApi.js index 0442db7..c2f0ead 100644 --- a/app/composables/useApi.js +++ b/app/composables/useApi.js @@ -84,6 +84,31 @@ export function useApi() { return { timeStr, nonce } } + async function fetchJson(url, options = {}) { + const response = await fetch(url, options) + const text = await response.text() + let result = null + + if (text) { + try { + result = JSON.parse(text) + } catch { + const err = new Error(text || `请求失败 (${response.status})`) + err.code = response.status + throw err + } + } + + if (!response.ok) { + const err = new Error(result?.msg || result?.message || `请求失败 (${response.status})`) + err.code = result?.code || response.status + err.response = result + throw err + } + + return result || {} + } + /** * 发起请求 */ @@ -95,7 +120,10 @@ export function useApi() { * 服务端(SSR)直接请求后端,避免内部再走一次 Nitro 代理时 * form 请求体丢失导致后端报「client必填」。 */ - const apiBase = useRuntimeConfig().public.apiBase + const runtimeConfig = useRuntimeConfig() + const apiBase = import.meta.server + ? (runtimeConfig.apiServerBase || runtimeConfig.public.apiBase) + : runtimeConfig.public.apiBase const url = import.meta.server ? apiBase + path : '/api' + path const timestamp = Math.floor(Date.now() / 1000) @@ -143,7 +171,9 @@ export function useApi() { fetchOptions.query = allParams // GET 请求通过 server proxy 时,参数在 query 里 const finalUrl = url + (queryStr ? '?' + queryStr : '') - const result = await $fetch(finalUrl, fetchOptions) + const result = import.meta.server + ? await fetchJson(finalUrl, fetchOptions) + : await $fetch(finalUrl, fetchOptions) return handleResult(result) } else { // POST: body 为 x-www-form-urlencoded @@ -154,7 +184,9 @@ export function useApi() { }) fetchOptions.body = new URLSearchParams(postBody).toString() - const result = await $fetch(url, fetchOptions) + const result = import.meta.server + ? await fetchJson(url, fetchOptions) + : await $fetch(url, fetchOptions) return handleResult(result) } } diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index b08be09..a346e34 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -2,13 +2,15 @@ const appName = process.env.APP_NAME || 'pc_nuxt' const host = process.env.HOST || '0.0.0.0' const port = process.env.PORT || '6888' const apiBase = process.env.NUXT_PUBLIC_API_BASE || 'https://api.aionline.cc' +const apiServerBase = process.env.NUXT_API_SERVER_BASE || apiBase const env = { NODE_ENV: 'production', HOST: host, PORT: port, NITRO_HOST: host, NITRO_PORT: port, - NUXT_PUBLIC_API_BASE: apiBase + NUXT_PUBLIC_API_BASE: apiBase, + NUXT_API_SERVER_BASE: apiServerBase } module.exports = { diff --git a/nuxt.config.js b/nuxt.config.js index 8c2eb20..7983330 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -36,11 +36,28 @@ function resolveApiBase() { return value } +function resolveServerApiBase() { + const value = String(process.env.NUXT_API_SERVER_BASE || apiBase).trim().replace(/\/+$/, '') + let url + try { + url = new URL(value) + } catch { + throw new Error(`[pc_nuxt] NUXT_API_SERVER_BASE 不是有效 URL:${value || '(empty)'}`) + } + + if (!['http:', 'https:'].includes(url.protocol)) { + throw new Error(`[pc_nuxt] NUXT_API_SERVER_BASE 仅支持 http/https:${value}`) + } + + return value +} + if (process.env.API_BASE && !process.env.NUXT_PUBLIC_API_BASE) { console.warn('[pc_nuxt] API_BASE 已废弃,请改用 NUXT_PUBLIC_API_BASE') } const apiBase = resolveApiBase() +const apiServerBase = resolveServerApiBase() // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ @@ -72,6 +89,8 @@ export default defineNuxtConfig({ // 运行时公开配置(客户端可访问) runtimeConfig: { + // 服务端 SSR / Nitro 代理专用后端地址。可设为 http://127.0.0.1:16888,避免服务端回打公网域名。 + apiServerBase, public: { // ThinkJS ai_api 地址,按环境覆盖 apiBase, diff --git a/restart.sh b/restart.sh index b28a201..f951a61 100644 --- a/restart.sh +++ b/restart.sh @@ -9,9 +9,11 @@ set -Eeuo pipefail APP_NAME="${APP_NAME:-pc_nuxt}" BRANCH="${BRANCH:-}" NUXT_PUBLIC_API_BASE="${NUXT_PUBLIC_API_BASE:-https://api.aionline.cc}" +NUXT_API_SERVER_BASE="${NUXT_API_SERVER_BASE:-$NUXT_PUBLIC_API_BASE}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" export NUXT_PUBLIC_API_BASE +export NUXT_API_SERVER_BASE cd "$SCRIPT_DIR" @@ -53,6 +55,23 @@ if (!['http:', 'https:'].includes(url.protocol) || isPrivate) { NODE } +validate_server_api_base() { + node - "$NUXT_API_SERVER_BASE" <<'NODE' +const value = process.argv[2] +let url +try { + url = new URL(value) +} catch { + console.error(`[pc_nuxt] Invalid NUXT_API_SERVER_BASE: ${value || '(empty)'}`) + process.exit(1) +} +if (!['http:', 'https:'].includes(url.protocol)) { + console.error(`[pc_nuxt] NUXT_API_SERVER_BASE must be http(s): ${value}`) + process.exit(1) +} +NODE +} + validate_tls_security() { if [ "${NODE_TLS_REJECT_UNAUTHORIZED:-}" = "0" ]; then log "Refusing deployment: NODE_TLS_REJECT_UNAUTHORIZED=0 disables TLS certificate verification." @@ -110,6 +129,7 @@ main() { require_command pm2 validate_tls_security validate_api_base + validate_server_api_base resolve_branch log "Deploy start: $APP_NAME" @@ -117,6 +137,7 @@ main() { log "pnpm: $(pnpm -v)" log "pm2: $(pm2 -v)" log "API: $NUXT_PUBLIC_API_BASE" + log "Server API: $NUXT_API_SERVER_BASE" sync_latest_code install_dependencies diff --git a/server/api/[...].js b/server/api/[...].js index c555f5b..1d04e98 100644 --- a/server/api/[...].js +++ b/server/api/[...].js @@ -12,9 +12,25 @@ */ import { proxyRequest, sendWebResponse } from 'h3' +async function parseUpstreamResponse(response) { + const contentType = response.headers.get('content-type') || '' + const text = await response.text() + + if (contentType.includes('application/json')) { + return text ? JSON.parse(text) : {} + } + + if (!text) return {} + try { + return JSON.parse(text) + } catch { + return text + } +} + export default defineEventHandler(async (event) => { const config = useRuntimeConfig(event) - const apiBase = String(config.public.apiBase || '').replace(/\/+$/, '') + const apiBase = String(config.apiServerBase || config.public.apiBase || '').replace(/\/+$/, '') if (!apiBase) { throw createError({ statusCode: 500, @@ -83,7 +99,8 @@ export default defineEventHandler(async (event) => { const upstreamResponse = await fetch(fullUrl, fetchOptions) return sendWebResponse(event, upstreamResponse) } - return await $fetch(fullUrl, fetchOptions) + const upstreamResponse = await fetch(fullUrl, fetchOptions) + return await parseUpstreamResponse(upstreamResponse) } catch (error) { console.error('[Server Proxy Error]', url, error.message) return {