plan-session_4489eda9b3f20102402e7c5a918902ee.md 11 KB

目标

桌面端启动时先绑定后端服务地址,绑定成功且服务可达后,才进入原有的「健康检查 → 授权校验 → 登录」流程。

总体流程

启动
 ├─ 未绑定 && 构建开启绑定要求
 │    → /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 内每次解析:

      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() 之前插入:

    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

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。