| @@ -84,6 +84,31 @@ export function useApi() { | |||||
| return { timeStr, nonce } | 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 代理时 | * 服务端(SSR)直接请求后端,避免内部再走一次 Nitro 代理时 | ||||
| * form 请求体丢失导致后端报「client必填」。 | * 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 url = import.meta.server ? apiBase + path : '/api' + path | ||||
| const timestamp = Math.floor(Date.now() / 1000) | const timestamp = Math.floor(Date.now() / 1000) | ||||
| @@ -143,7 +171,9 @@ export function useApi() { | |||||
| fetchOptions.query = allParams | fetchOptions.query = allParams | ||||
| // GET 请求通过 server proxy 时,参数在 query 里 | // GET 请求通过 server proxy 时,参数在 query 里 | ||||
| const finalUrl = url + (queryStr ? '?' + queryStr : '') | 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) | return handleResult(result) | ||||
| } else { | } else { | ||||
| // POST: body 为 x-www-form-urlencoded | // POST: body 为 x-www-form-urlencoded | ||||
| @@ -154,7 +184,9 @@ export function useApi() { | |||||
| }) | }) | ||||
| fetchOptions.body = new URLSearchParams(postBody).toString() | 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) | 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 host = process.env.HOST || '0.0.0.0' | ||||
| const port = process.env.PORT || '6888' | const port = process.env.PORT || '6888' | ||||
| const apiBase = process.env.NUXT_PUBLIC_API_BASE || 'https://api.aionline.cc' | const apiBase = process.env.NUXT_PUBLIC_API_BASE || 'https://api.aionline.cc' | ||||
| const apiServerBase = process.env.NUXT_API_SERVER_BASE || apiBase | |||||
| const env = { | const env = { | ||||
| NODE_ENV: 'production', | NODE_ENV: 'production', | ||||
| HOST: host, | HOST: host, | ||||
| PORT: port, | PORT: port, | ||||
| NITRO_HOST: host, | NITRO_HOST: host, | ||||
| NITRO_PORT: port, | NITRO_PORT: port, | ||||
| NUXT_PUBLIC_API_BASE: apiBase | |||||
| NUXT_PUBLIC_API_BASE: apiBase, | |||||
| NUXT_API_SERVER_BASE: apiServerBase | |||||
| } | } | ||||
| module.exports = { | module.exports = { | ||||
| @@ -36,11 +36,28 @@ function resolveApiBase() { | |||||
| return value | 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) { | if (process.env.API_BASE && !process.env.NUXT_PUBLIC_API_BASE) { | ||||
| console.warn('[pc_nuxt] API_BASE 已废弃,请改用 NUXT_PUBLIC_API_BASE') | console.warn('[pc_nuxt] API_BASE 已废弃,请改用 NUXT_PUBLIC_API_BASE') | ||||
| } | } | ||||
| const apiBase = resolveApiBase() | const apiBase = resolveApiBase() | ||||
| const apiServerBase = resolveServerApiBase() | |||||
| // https://nuxt.com/docs/api/configuration/nuxt-config | // https://nuxt.com/docs/api/configuration/nuxt-config | ||||
| export default defineNuxtConfig({ | export default defineNuxtConfig({ | ||||
| @@ -72,6 +89,8 @@ export default defineNuxtConfig({ | |||||
| // 运行时公开配置(客户端可访问) | // 运行时公开配置(客户端可访问) | ||||
| runtimeConfig: { | runtimeConfig: { | ||||
| // 服务端 SSR / Nitro 代理专用后端地址。可设为 http://127.0.0.1:16888,避免服务端回打公网域名。 | |||||
| apiServerBase, | |||||
| public: { | public: { | ||||
| // ThinkJS ai_api 地址,按环境覆盖 | // ThinkJS ai_api 地址,按环境覆盖 | ||||
| apiBase, | apiBase, | ||||
| @@ -9,9 +9,11 @@ set -Eeuo pipefail | |||||
| APP_NAME="${APP_NAME:-pc_nuxt}" | APP_NAME="${APP_NAME:-pc_nuxt}" | ||||
| BRANCH="${BRANCH:-}" | BRANCH="${BRANCH:-}" | ||||
| NUXT_PUBLIC_API_BASE="${NUXT_PUBLIC_API_BASE:-https://api.aionline.cc}" | 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)" | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||||
| export NUXT_PUBLIC_API_BASE | export NUXT_PUBLIC_API_BASE | ||||
| export NUXT_API_SERVER_BASE | |||||
| cd "$SCRIPT_DIR" | cd "$SCRIPT_DIR" | ||||
| @@ -53,6 +55,23 @@ if (!['http:', 'https:'].includes(url.protocol) || isPrivate) { | |||||
| NODE | 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() { | validate_tls_security() { | ||||
| if [ "${NODE_TLS_REJECT_UNAUTHORIZED:-}" = "0" ]; then | if [ "${NODE_TLS_REJECT_UNAUTHORIZED:-}" = "0" ]; then | ||||
| log "Refusing deployment: NODE_TLS_REJECT_UNAUTHORIZED=0 disables TLS certificate verification." | log "Refusing deployment: NODE_TLS_REJECT_UNAUTHORIZED=0 disables TLS certificate verification." | ||||
| @@ -110,6 +129,7 @@ main() { | |||||
| require_command pm2 | require_command pm2 | ||||
| validate_tls_security | validate_tls_security | ||||
| validate_api_base | validate_api_base | ||||
| validate_server_api_base | |||||
| resolve_branch | resolve_branch | ||||
| log "Deploy start: $APP_NAME" | log "Deploy start: $APP_NAME" | ||||
| @@ -117,6 +137,7 @@ main() { | |||||
| log "pnpm: $(pnpm -v)" | log "pnpm: $(pnpm -v)" | ||||
| log "pm2: $(pm2 -v)" | log "pm2: $(pm2 -v)" | ||||
| log "API: $NUXT_PUBLIC_API_BASE" | log "API: $NUXT_PUBLIC_API_BASE" | ||||
| log "Server API: $NUXT_API_SERVER_BASE" | |||||
| sync_latest_code | sync_latest_code | ||||
| install_dependencies | install_dependencies | ||||
| @@ -12,9 +12,25 @@ | |||||
| */ | */ | ||||
| import { proxyRequest, sendWebResponse } from 'h3' | 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) => { | export default defineEventHandler(async (event) => { | ||||
| const config = useRuntimeConfig(event) | const config = useRuntimeConfig(event) | ||||
| const apiBase = String(config.public.apiBase || '').replace(/\/+$/, '') | |||||
| const apiBase = String(config.apiServerBase || config.public.apiBase || '').replace(/\/+$/, '') | |||||
| if (!apiBase) { | if (!apiBase) { | ||||
| throw createError({ | throw createError({ | ||||
| statusCode: 500, | statusCode: 500, | ||||
| @@ -83,7 +99,8 @@ export default defineEventHandler(async (event) => { | |||||
| const upstreamResponse = await fetch(fullUrl, fetchOptions) | const upstreamResponse = await fetch(fullUrl, fetchOptions) | ||||
| return sendWebResponse(event, upstreamResponse) | return sendWebResponse(event, upstreamResponse) | ||||
| } | } | ||||
| return await $fetch(fullUrl, fetchOptions) | |||||
| const upstreamResponse = await fetch(fullUrl, fetchOptions) | |||||
| return await parseUpstreamResponse(upstreamResponse) | |||||
| } catch (error) { | } catch (error) { | ||||
| console.error('[Server Proxy Error]', url, error.message) | console.error('[Server Proxy Error]', url, error.message) | ||||
| return { | return { | ||||