| @@ -0,0 +1,5 @@ | |||
| # Local development API. Copy this file to .env when a different address is needed. | |||
| NUXT_PUBLIC_API_BASE=http://192.168.31.168:16888 | |||
| # Public site origin used for canonical URLs when configured. | |||
| NUXT_PUBLIC_SITE_URL=http://localhost:6888 | |||
| @@ -96,17 +96,27 @@ const res = await api.post('/user/login', { username, password }) | |||
| | --- | --- | | |||
| | `0` | 成功,数据在 `data` / `list` 字段 | | |||
| | `1000` | 业务异常,错误信息在 `msg` | | |||
| | `1009` | 登录失效,客户端自动跳转 `/login` | | |||
| | `1009` | 登录失效,客户端清除登录态并打开当前页面的登录弹窗 | | |||
| ### 配置后端地址 | |||
| ```bash | |||
| # 环境变量(优先级最高) | |||
| API_BASE=http://192.168.1.100:8360 pnpm run dev | |||
| NUXT_PUBLIC_API_BASE=http://192.168.1.100:8360 pnpm run dev | |||
| # 或直接修改 nuxt.config.js 中的 runtimeConfig.public.apiBase | |||
| # 未设置时,开发环境默认使用 http://192.168.31.168:16888 | |||
| # 也可以复制 .env.example 为 .env 后修改;不要直接修改 nuxt.config.js | |||
| ``` | |||
| 生产环境默认使用 `https://api.aionline.cc`。如需覆盖,必须使用公开的 HTTP(S) 地址: | |||
| ```bash | |||
| NUXT_PUBLIC_API_BASE=https://api.aionline.cc ./restart.sh | |||
| ``` | |||
| `restart.sh` 会在拉取和构建前校验该地址,拒绝 localhost、环回地址和局域网地址;同一变量会同时传入 Nuxt 构建、Nitro 运行时和 PM2,避免 SSR 与浏览器代理使用不同后端。 | |||
| 发布脚本也会拒绝 `NODE_TLS_REJECT_UNAUTHORIZED=0`,生产机应保持 TLS 证书校验开启。 | |||
| ### 服务端代理 | |||
| 前端调 `/api/**` 会自动代理到 `ai_api`,路径示例: | |||
| @@ -407,7 +407,11 @@ const strengthCopy = computed(() => '推荐 0.6 - 0.8,保留人脸特征') | |||
| // ==================== Upload ==================== | |||
| function triggerUpload() { | |||
| if (!user.isLogin.value) { showToast('请先登录'); router.push('/login'); return } | |||
| if (!user.isLogin.value) { | |||
| showToast('请先登录') | |||
| user.showLogin.value = true | |||
| return | |||
| } | |||
| fileInput.value?.click() | |||
| } | |||
| function onFileChange(e) { | |||
| @@ -518,6 +522,9 @@ onMounted(() => { | |||
| loadDemo() | |||
| if (user.isLogin.value) loadBalance() | |||
| }) | |||
| watch(() => user.isLogin.value, (loggedIn) => { | |||
| if (loggedIn) loadBalance() | |||
| }) | |||
| onUnmounted(() => clearPoll()) | |||
| // ==================== 历史记录 / 详情 ==================== | |||
| @@ -661,7 +668,11 @@ async function startPoll() { | |||
| } | |||
| async function handleSubmit() { | |||
| if (!user.isLogin.value) { showToast('请先登录'); router.push('/login'); return } | |||
| if (!user.isLogin.value) { | |||
| showToast('请先登录') | |||
| user.showLogin.value = true | |||
| return | |||
| } | |||
| if (uploading.value) { showToast('图片正在上传'); return } | |||
| if (!form.imgs.length) { showToast('请上传参考图'); return } | |||
| if (submitting.value) return | |||
| @@ -855,6 +855,9 @@ async function loadBalance() { | |||
| user.refreshBalance() | |||
| } | |||
| onMounted(() => { loadBilling(); if (user.isLogin.value) loadBalance() }) | |||
| watch(() => user.isLogin.value, (loggedIn) => { | |||
| if (loggedIn) loadBalance() | |||
| }) | |||
| const submitting = ref(false) | |||
| const questionTitle = ref('') | |||
| @@ -880,7 +883,11 @@ const streamContent = ref('') | |||
| const streamPreviewRef = ref(null) | |||
| async function handleSubmit() { | |||
| if (!user.isLogin.value) { showToast('请先登录'); return router.push('/login') } | |||
| if (!user.isLogin.value) { | |||
| showToast('请先登录') | |||
| user.showLogin.value = true | |||
| return | |||
| } | |||
| if (!form.surname) { showToast('请输入姓氏'); return } | |||
| if (submitting.value) return | |||
| syncBirthFields() | |||
| @@ -1,14 +1,14 @@ | |||
| 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.API_BASE || 'https://api.aionline.cc' | |||
| const apiBase = process.env.NUXT_PUBLIC_API_BASE || 'https://api.aionline.cc' | |||
| const env = { | |||
| NODE_ENV: 'production', | |||
| HOST: host, | |||
| PORT: port, | |||
| NITRO_HOST: host, | |||
| NITRO_PORT: port, | |||
| API_BASE: apiBase | |||
| NUXT_PUBLIC_API_BASE: apiBase | |||
| } | |||
| module.exports = { | |||
| @@ -1,5 +1,47 @@ | |||
| import { fileURLToPath } from 'node:url' | |||
| const isProduction = process.env.NODE_ENV === 'production' | |||
| const defaultApiBase = isProduction | |||
| ? 'https://api.aionline.cc' | |||
| : 'http://192.168.31.168:16888' | |||
| function resolveApiBase() { | |||
| const value = String(process.env.NUXT_PUBLIC_API_BASE || defaultApiBase).trim().replace(/\/+$/, '') | |||
| let url | |||
| try { | |||
| url = new URL(value) | |||
| } catch { | |||
| throw new Error(`[pc_nuxt] NUXT_PUBLIC_API_BASE 不是有效 URL:${value || '(empty)'}`) | |||
| } | |||
| if (!['http:', 'https:'].includes(url.protocol)) { | |||
| throw new Error(`[pc_nuxt] NUXT_PUBLIC_API_BASE 仅支持 http/https:${value}`) | |||
| } | |||
| const hostname = url.hostname.toLowerCase() | |||
| const isPrivateHost = hostname === 'localhost' | |||
| || hostname === '0.0.0.0' | |||
| || /^127\./.test(hostname) | |||
| || hostname === '[::1]' | |||
| || hostname.endsWith('.local') | |||
| || /^10\./.test(hostname) | |||
| || /^192\.168\./.test(hostname) | |||
| || /^169\.254\./.test(hostname) | |||
| || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) | |||
| if (isProduction && isPrivateHost) { | |||
| throw new Error(`[pc_nuxt] 生产环境禁止使用本地/局域网 API:${value},请设置 NUXT_PUBLIC_API_BASE`) | |||
| } | |||
| 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() | |||
| // https://nuxt.com/docs/api/configuration/nuxt-config | |||
| export default defineNuxtConfig({ | |||
| compatibilityDate: '2025-07-15', | |||
| @@ -32,7 +74,7 @@ export default defineNuxtConfig({ | |||
| runtimeConfig: { | |||
| public: { | |||
| // ThinkJS ai_api 地址,按环境覆盖 | |||
| apiBase: process.env.API_BASE || 'http://192.168.31.168:16888', | |||
| apiBase, | |||
| // 邀请链接正式域名;未配置时自动使用当前访问域名。 | |||
| siteUrl: process.env.NUXT_PUBLIC_SITE_URL || '', | |||
| }, | |||
| @@ -4,8 +4,11 @@ set -Eeuo pipefail | |||
| APP_NAME="${APP_NAME:-pc_nuxt}" | |||
| BRANCH="${BRANCH:-}" | |||
| NUXT_PUBLIC_API_BASE="${NUXT_PUBLIC_API_BASE:-https://api.aionline.cc}" | |||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |||
| export NUXT_PUBLIC_API_BASE | |||
| cd "$SCRIPT_DIR" | |||
| log() { | |||
| @@ -19,6 +22,40 @@ require_command() { | |||
| fi | |||
| } | |||
| validate_api_base() { | |||
| node - "$NUXT_PUBLIC_API_BASE" <<'NODE' | |||
| const value = process.argv[2] | |||
| let url | |||
| try { | |||
| url = new URL(value) | |||
| } catch { | |||
| console.error(`[pc_nuxt] Invalid NUXT_PUBLIC_API_BASE: ${value || '(empty)'}`) | |||
| process.exit(1) | |||
| } | |||
| const hostname = url.hostname.toLowerCase() | |||
| const isPrivate = hostname === 'localhost' | |||
| || hostname === '0.0.0.0' | |||
| || /^127\./.test(hostname) | |||
| || hostname === '[::1]' | |||
| || hostname.endsWith('.local') | |||
| || /^10\./.test(hostname) | |||
| || /^192\.168\./.test(hostname) | |||
| || /^169\.254\./.test(hostname) | |||
| || /^172\.(1[6-9]|2\d|3[01])\./.test(hostname) | |||
| if (!['http:', 'https:'].includes(url.protocol) || isPrivate) { | |||
| console.error(`[pc_nuxt] Production API must be a public http(s) URL: ${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." | |||
| exit 1 | |||
| fi | |||
| } | |||
| ensure_clean_worktree() { | |||
| if [ -n "$(git status --porcelain)" ]; then | |||
| log "Working tree is not clean. Commit/stash/clean changes before deploy." | |||
| @@ -71,12 +108,15 @@ main() { | |||
| require_command node | |||
| require_command pnpm | |||
| require_command pm2 | |||
| validate_tls_security | |||
| validate_api_base | |||
| resolve_branch | |||
| log "Deploy start: $APP_NAME" | |||
| log "Node: $(node -v 2>/dev/null || echo 'unknown')" | |||
| log "pnpm: $(pnpm -v)" | |||
| log "pm2: $(pm2 -v)" | |||
| log "API: $NUXT_PUBLIC_API_BASE" | |||
| pull_latest_code | |||
| install_dependencies | |||
| @@ -8,12 +8,19 @@ | |||
| * 1. 避免浏览器跨域 | |||
| * 2. 服务端转发自动携带 cookie,保持登录态 | |||
| * | |||
| * 环境变量 API_BASE 可覆盖目标地址(生产环境指向 https://api.aionline.cc) | |||
| * 环境变量 NUXT_PUBLIC_API_BASE 可覆盖目标地址(生产环境指向 https://api.aionline.cc) | |||
| */ | |||
| import { proxyRequest, sendWebResponse } from 'h3' | |||
| export default defineEventHandler(async (event) => { | |||
| const apiBase = process.env.API_BASE || 'http://192.168.31.168:16888' | |||
| const config = useRuntimeConfig(event) | |||
| const apiBase = String(config.public.apiBase || '').replace(/\/+$/, '') | |||
| if (!apiBase) { | |||
| throw createError({ | |||
| statusCode: 500, | |||
| statusMessage: 'NUXT_PUBLIC_API_BASE is not configured', | |||
| }) | |||
| } | |||
| // 去掉 /api 前缀,得到真实后端路径 | |||
| let path = event.path.replace(/^\/api/, '') || '/' | |||