diff --git a/package.json b/package.json index 341bfe1..0412c63 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "dev:electron": "concurrently \"set AUTH_SERVER_URL=http://127.0.0.1:3302&& set VITE_DEV_SERVER_URL=http://127.0.0.1:3354&& set EXTERNAL_ELECTRON_DEV=1&& vite --host 127.0.0.1 --port 3354 --strictPort\" \"set AUTH_SERVER_URL=http://127.0.0.1:3302&& set VITE_DEV_SERVER_URL=http://127.0.0.1:3354&& node scripts/dev-electron.cjs\"", "prepare-package": "node apply-oem.cjs && py -3 scripts/export-runtime-system-templates.py && node scripts/prepare-package.cjs", "build:win": "npm run prepare-package && npm run build && node scripts/build-resource-artifacts.cjs && electron-builder --win --x64 && node scripts/fix-latest-yml.cjs && node scripts/assemble-release-artifacts.cjs", + "build:client:win": "node scripts/package-client.cjs", "build:win:only": "electron-builder --win --x64", "build:mac": "npm run prepare-package && npm run typecheck && vite build && electron-builder --mac", "postinstall": "electron-builder install-app-deps", diff --git a/scripts/package-client.cjs b/scripts/package-client.cjs new file mode 100644 index 0000000..3afdc0d --- /dev/null +++ b/scripts/package-client.cjs @@ -0,0 +1,163 @@ +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +const projectRoot = path.resolve(__dirname, ".."); +const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"; +const electronBuilder = path.join( + projectRoot, + "node_modules", + ".bin", + process.platform === "win32" ? "electron-builder.cmd" : "electron-builder", +); + +function usage() { + console.log(` +客户端打包脚本(不包含认证服务/管理后台) + +用法: + npm.cmd run build:client:win -- --auth-url=https://auth.example.com + npm.cmd run build:client:win -- --auth-url=http://127.0.0.1:3302 --dry-run + +参数: + --auth-url 认证服务地址,可带或不带 /api + --dir 只打包目录,不生成安装包 + --dry-run 只打印将执行的命令,不真正打包 + --help 显示帮助 +`); +} + +function readArg(name) { + const prefix = `${name}=`; + const inline = process.argv.find(arg => arg.startsWith(prefix)); + if (inline) return inline.slice(prefix.length); + + const index = process.argv.indexOf(name); + if (index >= 0) return process.argv[index + 1]; + + return ""; +} + +function hasArg(name) { + return process.argv.includes(name); +} + +function normalizeAuthUrl(value) { + const raw = String(value || "").trim(); + if (!raw) return ""; + if (/^https?\/\//i.test(raw)) { + throw new Error(`认证服务地址无效,协议后缺少冒号:${raw}`); + } + + let parsed; + try { + parsed = new URL(raw); + } catch { + throw new Error(`认证服务地址无效:${raw}`); + } + + if (!["http:", "https:"].includes(parsed.protocol)) { + throw new Error(`认证服务地址只支持 http 或 https:${raw}`); + } + if (["http", "https"].includes(parsed.hostname) && parsed.pathname.startsWith("//")) { + throw new Error(`认证服务地址疑似多写了协议,请检查:${raw}`); + } + if (!parsed.hostname || parsed.hostname.includes("/")) { + throw new Error(`认证服务地址主机名无效:${raw}`); + } + + parsed.pathname = parsed.pathname.replace(/\/api\/?$/i, "").replace(/\/+$/, ""); + parsed.search = ""; + parsed.hash = ""; + return parsed.toString().replace(/\/+$/, ""); +} + +function quoteDisplay(value) { + const text = String(value); + return /\s/.test(text) ? `"${text}"` : text; +} + +function quoteCmdArg(value) { + const text = String(value); + if (!/[ \t"&|<>^]/.test(text)) return text; + return `"${text.replace(/"/g, '""')}"`; +} + +function isWindowsCmd(command) { + return process.platform === "win32" && /\.(cmd|bat)$/i.test(command); +} + +function run(command, args, env, dryRun) { + const display = [command, ...args].map(quoteDisplay).join(" "); + console.log(`[package-client] ${display}`); + if (dryRun) return; + + const spawnCommand = isWindowsCmd(command) + ? (process.env.ComSpec || "cmd.exe") + : command; + const spawnArgs = isWindowsCmd(command) + ? ["/d", "/s", "/c", [quoteCmdArg(command), ...args.map(quoteCmdArg)].join(" ")] + : args; + + const result = spawnSync(spawnCommand, spawnArgs, { + cwd: projectRoot, + stdio: "inherit", + env, + shell: false, + }); + + if (result.error) { + throw result.error; + } + + if (result.status !== 0) { + throw new Error(`命令执行失败:${display}`); + } +} + +function main() { + if (hasArg("--help") || hasArg("-h")) { + usage(); + return; + } + + const authUrl = normalizeAuthUrl( + readArg("--auth-url") || + process.env.PACKAGE_AUTH_SERVER_URL || + process.env.AUTH_SERVER_URL || + process.env.VITE_AUTH_SERVER_URL, + ); + const dryRun = hasArg("--dry-run"); + const dirOnly = hasArg("--dir"); + + if (!authUrl) { + usage(); + throw new Error("打包客户端必须指定认证服务地址:--auth-url=https://auth.example.com"); + } + + const env = { + ...process.env, + AUTH_SERVER_URL: authUrl, + VITE_AUTH_SERVER_URL: authUrl, + PACKAGE_CLIENT_ONLY: "1", + }; + + console.log(`[package-client] 认证服务地址:${authUrl}`); + console.log("[package-client] 本脚本只打包客户端,不会启动或打包本地认证服务。"); + + run(npmCommand, ["run", "prepare-package"], env, dryRun); + run(npmCommand, ["run", "build"], env, dryRun); + run(process.execPath, ["scripts/build-resource-artifacts.cjs"], env, dryRun); + run(electronBuilder, ["--win", "--x64", ...(dirOnly ? ["--dir"] : [])], env, dryRun); + + if (!dirOnly) { + run(process.execPath, ["scripts/fix-latest-yml.cjs"], env, dryRun); + run(process.execPath, ["scripts/assemble-release-artifacts.cjs"], env, dryRun); + } +} + +try { + main(); +} catch (error) { + console.error(`[package-client] ${error.message}`); + process.exit(1); +} diff --git a/src/config.ts b/src/config.ts index ff44148..622a82a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,6 +3,15 @@ import packageJson from "../package.json"; // import {TimeUtil} from "../electron/lib/util"; // ❌ 这是 Node.js 版本,会被打包到渲染进程 const BASE_URL = ""; +const viteEnv = import.meta.env || {}; +const processEnv = (typeof process !== "undefined" ? process.env : {}) as Record; +const authServerUrl = viteEnv.VITE_AUTH_SERVER_URL || processEnv.AUTH_SERVER_URL || "http://127.0.0.1:3302"; + +function normalizeApiBaseUrl(url: string): string { + const trimmed = String(url || "").trim().replace(/\/+$/, ""); + if (!trimmed) return "http://127.0.0.1:3302/api"; + return /\/api$/i.test(trimmed) ? trimmed : `${trimmed}/api`; +} export const AppConfig = { name: "天英超级IP智能体", @@ -12,7 +21,7 @@ export const AppConfig = { website: ``, websiteGithub: "", websiteGitee: "", - apiBaseUrl: `http://127.0.0.1:3302/api`, + apiBaseUrl: normalizeApiBaseUrl(authServerUrl), updaterUrl: ``, downloadUrl: ``, feedbackUrl: ``,