## 目标 桌面端启动时先绑定后端服务地址,绑定成功且服务可达后,才进入原有的「健康检查 → 授权校验 → 登录」流程。 ## 总体流程 ``` 启动 ├─ 未绑定 && 构建开启绑定要求 │ → /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/health` - `testServerConnection(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`。 ## 二、让所有请求真正跟随绑定地址(关键) 1. **改** `src/core/utils/http/axios/index.ts` - 删除模块级的 `const globSetting = useGlobSetting()` / `const urlPrefix = ...`。 - `createAxios` 的 `requestOptions` 里**删掉** `apiUrl` / `urlPrefix` 两个固定值。 - `beforeRequestHook` 内每次解析: ```ts const glob = useGlobSetting(); const apiUrl = options.apiUrl === undefined ? glob.apiUrl : options.apiUrl; const urlPrefix = options.urlPrefix === undefined ? glob.urlPrefix : options.urlPrefix; ``` 这样单次调用的 `apiUrl: ''`(`upload.ts` 里有)仍然生效,未传则用当前绑定地址。 2. **改** `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` 无循环依赖。 3. **改** `src/ai/utils/constants.ts` - `resolveApiUrl()` 改为调用时读取 setting;删除模块级 `AI_API_ORIGIN` / `AI_API_PREFIX` / `AI_ADMIN_PATH` / `CHAT_STREAM_URL`(全仓库仅本文件使用),新增 `getChatStreamUrl()`。 4. **改** `src/ai/api/sse.ts`、`src/graph/insight/api.ts` - SSE 地址由模块级常量改为调用时计算。 5. **改** 两个不走 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()` 之前插入: ```ts 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`。 ## 四、绑定页 UI **新建** - `src/core/components/ServerAddress/ServerAddressPanel.vue`:表单 + 状态 + 按钮(页面与设置弹窗共用同一份实现)。 - 输入框(`LinkOutlined` 前缀、`allow-clear`、回车即连接,自动聚焦),占位 `例如 http://192.168.1.100:8980` - 提示文案:只填协议、主机和端口,不要包含 `/js`、`/a` - 状态行:连接中 / 连接成功(耗时 N ms)/ 失败原因 - 按钮:`测试连接`、`保存并进入`(保存前必先测通,测不通就不写入,避免下次启动空等) - 已绑定时显示「当前地址」和次要的「清除已保存的地址」 - 保存逻辑:地址变化时先 `clearAuthCache()`(防止拿 A 服务器的 token 打 B 服务器)→ 写入 → `reloadToAppRoot()` - `src/plat/views/serverSetup/index.vue`:全屏页(复用 `AppHeader`,卡片样式贴近 `/authorization`),若 `?error=unreachable` 顶部显示「上次保存的服务地址无法连接,请确认服务已启动或重新填写」。 **改** `src/plat/views/authorization/index.vue` - 卡片下方加一行「服务地址:http://… 切换」→ 跳 `/server-setup`。这是绑错地址后唯一的自救入口(此时没有 token,进不了业务页和设置抽屉)。 **改** `src/core/layouts/default/setting/components/SettingFooter.vue` + 新建 `ServerAddressModal.vue` - 设置抽屉增加「服务地址」按钮,弹窗内嵌 `ServerAddressPanel`,供登录后切换服务器(例如测试 → 生产)。 ## 五、构建配置 **改** `ai-electron/frontend/.env.production` ```ini VITE_GLOB_REQUIRE_SERVER_SETUP = true ``` 只影响 `vite build --mode production`(即打包的桌面端);`.env.development` / `.env.tomcat` 不加,开发时继续走 Vite 代理、web 部署不受影响。`VITE_GLOB_API_URL` 保持不变,作为兜底默认值。 ## 六、后端 CORS **新建** `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=3600` - **`exposedHeaders` 必须包含 `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,不含路径)。 - 不改现有 401 / 授权过期 / 跨窗口 `AUTH_INVALID` 的处理。 ## 八、验证 1. `cd ai-electron/frontend && pnpm type:check`(若基线已有报错,确保本次涉及文件无新增)。 2. `cd ai-electron/frontend && pnpm build`,检查 `dist/_app.config.js` 含 `VITE_GLOB_REQUIRE_SERVER_SETUP`。 3. `cd ai-server && mvn -q -DskipTests compile`。 4. 开发态手测:起后端 → `pnpm dev` → 打开 `#/server-setup`: - 错误地址(如 `http://127.0.0.1:9999`)→ 中文失败提示,不写入; - `http://127.0.0.1:8980` → 连接成功并显示耗时 → 保存 → 落到授权/登录页; - 带路径输入(`http://127.0.0.1:8980/js`)→ 明确拒绝; - 登录后从设置抽屉改地址 → 清 token 并回授权页。 5. 打包冒烟(可选):`cd ai-electron && npm run build`,确认 `public/dist/_app.config.js` 带开关;装包后首次启动应停在绑定页。 6. 回归:已验证绑定后,离线授权上传(`.xlts`)、文件上传、AI 对话 SSE、Govern 进度 SSE、文件下载(文件名)、`/sys/map/**` 瓦片均可用。 ## 九、风险提示 - `useGlobSetting()` 仍会被约 30 个 API 模块在导入时调用,但它们只用 `adminPath`/`ctxPath`(纯路径),不取 `apiUrl`;唯一在模块级捕获 `apiUrl` 的三处已在第二节修掉。 - 绑定值存在 `localStorage`:Electron 下按 `file://` 源持久化,落在用户数据目录;dev 浏览器(`localhost:3100`)是另一份,互不影响。开发态一旦绑定,请求会绕过 Vite 代理,用「清除已保存的地址」恢复。 - 切换到不同后端必须清 token,否则旧 token 会先打新服务器再触发 401。 - 后端开启 `allowedOriginPatterns=*` + `allowCredentials=false` 不会引入 cookie 凭据跨域;若你们有固定域名,建议用环境变量收敛为具体 origin。