| @@ -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) | |||
| } | |||
| } | |||
| @@ -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 = { | |||
| @@ -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, | |||
| @@ -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 | |||
| @@ -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 { | |||