桌面端启动时先绑定后端服务地址,绑定成功且服务可达后,才进入原有的「健康检查 → 授权校验 → 登录」流程。
启动
├─ 未绑定 && 构建开启绑定要求
│ → /server-setup(输入地址 → 测试连接 → 保存)
│ → 重新加载 → 走下面已绑定的分支
└─ 已绑定
→ GET /js/a/sys/health(最多约 30s)
→ 失败 → 回到 /server-setup 并提示「上次的地址无法连接」
→ 成功 → GET /js/a/sys/checkAuth → 授权有效?→ /authorization(登录卡片)
→ 授权失效 → /authorization?skipAuthCheck=1
已确认的两个决策:独立启动页 /server-setup;后端新增 CORS 过滤器(桌面端 file:// 访问远端 http:// 属于跨域,且后端当前没有任何 CORS 配置,不放行的话「测试连接」和登录都会被浏览器拦掉)。
新建 ai-electron/frontend/src/core/settings/serverSetting.ts
localStorage 原始 key SERVER_BASE_URL__(不带版本号,升级后仍复用;不经 Persistent 的 AES 缓存,避免受「清除缓存」影响)。normalizeServerOrigin(raw):补全 http://(支持只输 192.168.1.1:8980)→ new URL 校验 → 只允许 http/https → 拒绝带路径/参数/hash 的输入,返回 http(s)://host[:port]。错误文案要具体(例如「不要带 /js、/a 等路径」)。getBoundServerOrigin() / setBoundServerOrigin() / clearBoundServerOrigin()getApiOrigin() = 绑定值 || 构建时 VITE_GLOB_API_URL(保留旧行为作为兜底)isServerBindingEnabled():读 VITE_GLOB_REQUIRE_SERVER_SETUP(注意 _app.config.js 里是字符串 "true",dev 下是布尔值,用宽松比较)isServerSetupMissing() = 开启绑定要求 && 无绑定值buildHealthCheckUrl(origin) = ${origin}${VITE_GLOB_API_URL_PREFIX}${VITE_GLOB_ADMIN_PATH}/sys/healthtestServerConnection(origin, timeoutMs = 8000):fetch + AbortController,解析 JSON 判 code === 200;错误映射成中文(超时 / 网络不可达或跨域被拦 / 非接口响应 / HTTP 状态码)reloadToAppRoot():location.replace(href.split('#')[0]),回到根路由后由守卫重新决定落地页改 src/core/hooks/setting/index.ts
globCache,改为按 getApiOrigin() 结果做缓存(cachedApiUrl !== origin 时重建)。ctxPath/adminPath 仍按原算法从路径推导,路径本身不变(/js、/a)。改 src/types/config.d.ts
GlobEnvConfig 增加 VITE_GLOB_REQUIRE_SERVER_SETUP?: string | boolean。改 src/core/utils/http/axios/index.ts
const globSetting = useGlobSetting() / const urlPrefix = ...。createAxios 的 requestOptions 里删掉 apiUrl / urlPrefix 两个固定值。beforeRequestHook 内每次解析:
const glob = useGlobSetting();
const apiUrl = options.apiUrl === undefined ? glob.apiUrl : options.apiUrl;
const urlPrefix = options.urlPrefix === undefined ? glob.urlPrefix : options.urlPrefix;
这样单次调用的 apiUrl: ''(upload.ts 里有)仍然生效,未传则用当前绑定地址。
改 src/core/utils/http/axios/Axios.ts → uploadFile()
axiosInstance.request,绕过了 beforeRequestHook,当前根本没有 origin(离线授权 /sys/noNet 就踩这个坑)。对相对 URL 前缀 useGlobSetting().apiUrl;已是 http(s):// 的绝对地址不动。已确认 Axios.ts → hooks/setting → utils/env → utils/log 无循环依赖。改 src/ai/utils/constants.ts
resolveApiUrl() 改为调用时读取 setting;删除模块级 AI_API_ORIGIN / AI_API_PREFIX / AI_ADMIN_PATH / CHAT_STREAM_URL(全仓库仅本文件使用),新增 getChatStreamUrl()。改 src/ai/api/sse.ts、src/graph/insight/api.ts
改 两个不走 defHttp 的直连地址(同样是相对路径 → file:// 下会失效)
src/core/components/Markdown/src/Markdown.vue:vditor upload.url 加 origin。src/core/components/WangEditor/src/WangEditor.vue:图片 ctxAdminPath + '/file/download/...' 加 origin。
governApi.getAdminBaseUrl()、openOfficeFile.buildDownloadUrl()、GovernProgressModal的 SSE 都是调用时读取 setting,改完自动生效;portrait.vue/CardList/Upload/props.ts只用路径部分,不受影响。
改 src/main.ts
在 setupRouterGuard() 之后、waitForBackendReady() 之前插入:
if (isServerSetupMissing()) {
if (isSmartWindow()) { await focusMainWindow(); void closeSmartWindow(); return; }
await router.replace(PageEnum.BASE_SERVER_SETUP);
await router.isReady();
setupGlobDirectives(app); setupErrorHandle(app); app.mount('#app');
return;
}
健康检查:绑定开启时预算从 60×2s 收紧到 20×1.5s(刚验证过的地址不需要等两分钟);失败则跳 /server-setup?error=unreachable,不再带着坏地址继续跑授权校验。绑定未开启(dev / tomcat)保持原行为。
getAuthorizedTarget() 增加 /server-setup → 落到 BASE_CASE,避免重启后停在绑定页。
改 src/core/enums/pageEnum.ts、src/core/router/routes/index.ts
BASE_SERVER_SETUP = '/server-setup' 与 ServerSetupRoute,放进 basicRoutes(静态路由,首次进入即可用)。改 两处守卫
guard/permissionGuard.ts:与 /authorization 一起放行。guard/index.ts 的 createWorkbenchGuard:智能窗口允许 /server-setup,否则会被拉回 /workbench。改 src/App.vue
ensureAuthorizationUnit() 在未绑定或无 origin 时直接返回,避免绑定页上仍去请求 /sys/info。新建
src/core/components/ServerAddress/ServerAddressPanel.vue:表单 + 状态 + 按钮(页面与设置弹窗共用同一份实现)。
LinkOutlined 前缀、allow-clear、回车即连接,自动聚焦),占位 例如 http://192.168.1.100:8980/js、/a测试连接、保存并进入(保存前必先测通,测不通就不写入,避免下次启动空等)clearAuthCache()(防止拿 A 服务器的 token 打 B 服务器)→ 写入 → reloadToAppRoot()src/plat/views/serverSetup/index.vue:全屏页(复用 AppHeader,卡片样式贴近 /authorization),若 ?error=unreachable 顶部显示「上次保存的服务地址无法连接,请确认服务已启动或重新填写」。改 src/plat/views/authorization/index.vue
/server-setup。这是绑错地址后唯一的自救入口(此时没有 token,进不了业务页和设置抽屉)。改 src/core/layouts/default/setting/components/SettingFooter.vue + 新建 ServerAddressModal.vue
ServerAddressPanel,供登录后切换服务器(例如测试 → 生产)。改 ai-electron/frontend/.env.production
VITE_GLOB_REQUIRE_SERVER_SETUP = true
只影响 vite build --mode production(即打包的桌面端);.env.development / .env.tomcat 不加,开发时继续走 Vite 代理、web 部署不受影响。VITE_GLOB_API_URL 保持不变,作为兜底默认值。
新建 ai-server/src/main/java/com/zsjz/ai/common/config/CorsConfig.java
@Configuration + @ConditionalOnProperty(name = "app.cors.enabled", havingValue = "true", matchIfMissing = true)CorsFilter Bean,@Order(Ordered.HIGHEST_PRECEDENCE)(必须早于 sa-token 的 -104,preflight 才能被正常应答)allowedOriginPatterns 取自 app.cors.allowed-origin-patterns(默认 *),allowedHeaders / allowedMethods 全放行,allowCredentials=false,maxAge=3600exposedHeaders 必须包含 Content-Disposition,否则跨域下 downloadFileApi 拿不到文件名改 ai-server/src/main/resources/application.yaml:补默认配置说明(app.cors.enabled: true、allowed-origin-patterns: "*")。
_app.config.js 的生成方式与 Object.freeze 逻辑。/authorization 的 license/login 两态。ctxPath/adminPath 的静态推导(绑定地址只允许 origin,不含路径)。AUTH_INVALID 的处理。cd ai-electron/frontend && pnpm type:check(若基线已有报错,确保本次涉及文件无新增)。cd ai-electron/frontend && pnpm build,检查 dist/_app.config.js 含 VITE_GLOB_REQUIRE_SERVER_SETUP。cd ai-server && mvn -q -DskipTests compile。pnpm dev → 打开 #/server-setup:
http://127.0.0.1:9999)→ 中文失败提示,不写入;http://127.0.0.1:8980 → 连接成功并显示耗时 → 保存 → 落到授权/登录页;http://127.0.0.1:8980/js)→ 明确拒绝;cd ai-electron && npm run build,确认 public/dist/_app.config.js 带开关;装包后首次启动应停在绑定页。.xlts)、文件上传、AI 对话 SSE、Govern 进度 SSE、文件下载(文件名)、/sys/map/** 瓦片均可用。useGlobSetting() 仍会被约 30 个 API 模块在导入时调用,但它们只用 adminPath/ctxPath(纯路径),不取 apiUrl;唯一在模块级捕获 apiUrl 的三处已在第二节修掉。localStorage:Electron 下按 file:// 源持久化,落在用户数据目录;dev 浏览器(localhost:3100)是另一份,互不影响。开发态一旦绑定,请求会绕过 Vite 代理,用「清除已保存的地址」恢复。allowedOriginPatterns=* + allowCredentials=false 不会引入 cookie 凭据跨域;若你们有固定域名,建议用环境变量收敛为具体 origin。