cc 6 napja
szülő
commit
45acb39460

+ 166 - 0
.zcode/plans/plan-session_1bc2a9baf5931f11f8e15fbbfd50834d.md

@@ -0,0 +1,166 @@
+# AI 数据清洗改造计划(修订版)
+
+已确认口径:**必填列任何数据错误(空值/类型不合法/函数执行异常/超长溢出)→ 整行拒绝**;**无模板自建表照常入库,但单独统计,不计入模板匹配成功**;**每一行为什么没进去必须可追溯**。
+
+## 一、评审结论(摘要)
+
+**匹配率**:表头归一化删除括号语义(`金额(元)`/`金额(万元)`、`账号(本方)`/`账号(对方)` 同指纹);打分集合匹配不看列序、无同义词、短模板难破 0.90;AI 模板复用不校验 md5/列序、不跑 verify;`header_md5` 全局唯一但物理表案件级;同批次同表头撞唯一键;列数不一致直接 drop 共享表;模型自评 0.6 可把 0 分候选升 STRONG;CSV/加密/多块/合并单元格探测缺口大。
+
+**入库率**:出口 B 无日期/时间转换(出口 A 有 `FunDate/FunTime/FunDateTime`);`DATE` 收到 datetime 串、`TIME` 被猜成 `DATE_TIME`,DuckDB cast 失败整批 FAIL;金额不支持负数/括号/币种;`NUMBER→INTEGER` 溢出;一个坏值让整批 INSERT 失败;required 来自模型、必填失败整行丢且不计数;异常路径不清理;统计把未清洗算成功。
+
+**类型匹配率**:类型只按英文字段名关键词猜;`ColStat.shape` 只进提示词没进建表;出口 B 忽略 `table_field` 类型;`directionConf/fieldLen` 丢失;`FunMultiplier` 整数 NPE、小数只乘整数部分。
+
+## 二、Commit 1(Phase A):类型化、行级诊断与入库止血
+
+### A0. 行级失败诊断与成功/失败统计(本次新增重点)
+
+**A0-1 行清洗结果模型**
+- 新增 `ColumnError`:`colIndex / columnName / reason / valuePreview / blocking`。
+- reason 枚举:`REQUIRED_EMPTY`、`REQUIRED_TYPE_INVALID`、`REQUIRED_FUNC_ERROR`、`REQUIRED_LENGTH_OVERFLOW`、`OPTIONAL_TYPE_INVALID`、`OPTIONAL_FUNC_ERROR`、`OPTIONAL_LENGTH_OVERFLOW`。
+- `AiRecordCleaner` 新增 `cleanRowDetailed(...)`:**遍历所有列、收集全部错误**,不再遇到第一个必填错误就返回;任一 `blocking=true` → 整行拒绝;只有非必填错误 → 单元格置 null、行照常入库。
+- 函数链异常不再被 `FunProcess.onException` 静默吞掉:在 AI 侧捕获 `FunException`,记录 `FUNC_ERROR`;必填列 → 行拒绝,非必填 → 列置空。
+- 必填规则:空值、日期/金额/数字/身份证/电话/卡号解析失败、函数异常、长度/精度溢出,全部整行拒绝(已确认)。
+
+**A0-2 失败行落库(可逐行追溯)**
+- 在案件 DuckDB 新增 `ai_clean_reject`(懒建表,`CREATE TABLE IF NOT EXISTS`):
+  ```
+  id, job_id, case_id, file_id, sheet_id, block_no, row_no,
+  blocking_col_index, blocking_col_name, reason,
+  errors_json,          -- 该行所有列错误(列名/原因/脱敏值预览)
+  create_time
+  ```
+  索引 `(job_id, row_no)`。
+- 每个被拒绝的行写一条记录;`blocking_col_index/name` 是第一个阻塞列,`errors_json` 保留该行全部列错误,能回答“因为哪一列没进去”。
+- 非必填错误(行已入库但单元格为空)默认只做逐列聚合计数 + 样例,不逐行落库;配置 `zsjz.ai-clean.reject-log-optional=true` 可逐行记录。
+- 批量写入(攒批 flush),避免逐行 SQL;任务失败时按 `ai_job_id` 清理半截记录;删除文件/撤销任务时同步清理。
+- 脱敏:身份证/手机号/卡号默认掩码(保留首尾),配置 `zsjz.ai-clean.mask-sensitive=false` 可关闭;值预览长度可配。
+
+**A0-3 日志**
+- 每行拒绝的权威记录在 `ai_clean_reject`;应用日志只打前 N 条(默认 20)避免刷爆:
+  `WARN AI reject job=... file=... sheet=... row=42 col=3(金额) reason=REQUIRED_TYPE_INVALID value=1.2万`。
+- 每个 sheet 结束打一条汇总:`source / loaded / rejected / warned / topReasons / topColumns`。
+- 配置 `zsjz.ai-clean.reject-log-sample`(默认 20)、`reject-log-all`(默认 false,调试用)。
+- 写入异常(BatchWriter/连接):记录 `jobId`、当前 `rowNo`、异常信息;该行记 `WRITER_ERROR`,剩余行计 `row_unprocessed`。
+
+**A0-4 成功/失败条数口径**
+- `ai_clean_job` 新增:`row_total`(表头后非空源行)、`row_loaded`、`row_rejected`,新增 `row_warned`、`row_unprocessed`。
+- 成功任务:`row_loaded + row_rejected = row_total`;`row_warned` 是已入库但含非必填错误的行数(不重复计入失败)。
+- 失败任务:半截新行按 job 清理,`row_loaded=0`(最终表没有新增行);`row_rejected=已处理且被拒的行`;`row_unprocessed=row_total-已处理行`。
+- `job.detail.load` 写 JSON:`reasonCounts`、`columnRejectCounts`、`optionalErrorCounts`、`topExamples`(脱敏)。
+- `ai_clean_batch` 新增:`loaded_rows`、`rejected_rows`、`self_built_count`;`SmartCleanStatusVO` 下发。
+- 终态/`getFiles`:SUCCESS 用 `rowTotal/rowLoaded/rowRejected`;NEED_REVIEW/SKIP/FAIL 不再用 `file_info.dataNum` 计成功,成功数记 0;自建表单独展示。
+- SSE 终态:`completed = 既有清洗成功行 + AI rowLoaded`;`failed = 既有失败行 + AI rowRejected`;未清洗单独计数,不混入成功。
+
+**A0-5 查询接口与前端**
+- 新增 `GET /aiClean/job/rejections?jobId=&page=&size=&reason=`,分页返回 `row_no / 列名 / 原因 / 脱敏值 / errors_json`。
+- `jobs` 接口返回 `rowTotal/rowLoaded/rowRejected/rowWarned` 与 `detail.load` 汇总。
+- 前端:AI 表结果页/导入文件管理增加「失败明细」抽屉或表格(复用现有表格组件),显示成功/失败/警告条数与逐行原因;先做只读查看。
+
+### A1. 统一类型转换器
+新增 `AiValueCoercer`,`AiRecordCleaner` 调用,输出 Java 类型:
+- DATE → `LocalDate`、TIME → `LocalTime`、DATE_TIME → `LocalDateTime`;支持 ISO、`yyyy/M/d`、`yyyy.MM.dd`、`yyyyMMdd`、中文日期、带时间/ISO、纯时间;DATE 取日期部分、DATE_TIME 纯日期补 00:00、TIME 从 datetime 取时间;严格解析;文本 Excel 序列号仅在 DATE/DATE_TIME 且 1..2958465 兜底;10/13 位时间戳仅在表头/字段名含“时间戳/timestamp”且范围合理时解析。
+- MONEY → `BigDecimal`:全角转半角;去 `¥¥$€£元`/空格;支持正负号、括号负数;千分位校验后去除;支持科学计数法;百分号默认拒绝;`setScale(2, HALF_UP)`;整数位 >16 按坏值处理。
+- NUMBER → `BigDecimal`:千分位/科学计数法;按 `DECIMAL(38,6)` 校验精度。
+- PHONE/ACCT_CARD_NO/ID_CARD_NUMBER:电话支持 `+86`/空格/横线;卡号拒绝科学计数法、校验 13–19 位(可选 Luhn);身份证支持 15/18 位并校验出生日期/校验位;失败返回 null 而非空串。
+- `fieldType` 统一 trim+uppercase;类型化列使用保留 `+/-/.` 的清洗规则,TEXT 保持既有行为。
+- 初始化 `GlobalCache.FUN_REGULAR_MAP` 五个具名正则(`EXTRACT_NAMED` 目前空实现);未知 key fail-open 并记日志。
+
+### A2. 类型来源与 ColStat
+- 优先级:出口 A `table_field` → AI 模板 `ai_clean_field` → `ColStat.shape` → 表头关键词 → TEXT。
+- `AiRulePlan.forAi` 接收 `List<ColStat>` 与候选模板:绑定列用模板类型/required/fieldLen/directionConf;未绑定列用形状;自建表全部用形状+关键词。
+- `forStoredTemplate` 填 `directionConf`;`forExisting` 补 `zf/directionConf`;新增 `forExistingColumns` 供 verify。
+- `typeOf` 改词边界/中文关键词,补 TIME/NUMBER,修 `card_no/id_card/cert_no/accountNo`,避免 `updateUser/candidate/totalTime` 误判。
+- 修 `ColStatBuilder.shape`:日期时间/纯时间、负/括号/币种金额、13–19 位卡号不判 NUMBER、带前缀手机号;日期规则用 `find`/锚定正则。
+- 自建表按形状建 DATE/TIME/TIMESTAMP/DECIMAL 列,不再全 TEXT。
+
+### A3. DDL 与数值范围
+- `DuckTypeMapper`:MONEY → `DECIMAL(18,2)`(保持兼容,入库校验 16 位整数);NUMBER → `DECIMAL(38,6)`;显式列出 PHONE/ACCT/ID/DEBIT/CALL → TEXT;AI 表不使用 `VARCHAR(n)`,避免超长整批失败。
+- `FunMultiplier` 用 `BigDecimal.multiply` 重写;`SCALE` 整数/小数一致。
+- 入库前校验 DECIMAL 精度/长度,超范围按坏值处理,不让 DuckDB 抛到批处理层。
+
+### A4. 校验与置信度
+- `verify` 使用与 load 相同的执行计划(出口 A `forExistingColumns`、复用 `forStoredTemplate`、新建 `forAi`);抽 `AiRulePlan.resolve(...)` 一处生成。
+- `requiredFillRate` 改为逐行 required 非空率;`llmScore` 传 `draft.confidence`。
+- 任何 required 列被抽样拒绝(错误率 >5%)→ 直接 NeedReview,不静默删列。
+- 复用 AI 模板也跑一次抽样 verify(raw 表 300 行,无模型调用);精确 md5 直通只跳过模型,不跳过类型/必填检查。
+
+### A5. 失败隔离与清理
+- 新表:异常/零行/注册失败 → drop 新表,不登记。
+- 复用表:先写新 job 的行,成功后删除同 `file_id+sheet_id` 且 `ai_job_id != 当前` 的旧行;失败时 `deleteByJob` 清理半截行,旧行保留。
+- 复用表不存在 → 按存档字段在当前案件 `createAiTable`;失败则 FAIL,不跨案件写。
+- 拒绝记录按 job 清理;删除文件/撤销任务同步清理 `ai_clean_reject`。
+
+### A6. 数据库变更
+- `ai_clean_job`:`row_rejected`、`row_warned`、`row_unprocessed`;`ai_clean_batch`:`loaded_rows`、`rejected_rows`、`self_built_count`。
+- 案件 DuckDB:懒建 `ai_clean_reject`。
+- 更新 `sql/ai_clean.sql` + 幂等迁移脚本/启动迁移(`ALTER TABLE ... ADD COLUMN IF NOT EXISTS`)。
+
+### A7. 测试
+- 格式矩阵:日期/时间/金额/数字/电话/身份证/卡号;必填失败整行拒绝、非必填置空计数。
+- 行级诊断:10 行样本中 3 行必填坏值 + 1 行非必填坏值 → `row_total=10, row_loaded=7, row_rejected=3, row_warned=1`;`ai_clean_reject` 3 条,`row_no/列名/reason/errors_json` 正确;API 分页可查。
+- 一行多列错误 → 记录全部列错误,`blocking` 列取第一个。
+- 函数异常(SCALE/EXTRACT)必填 → 行拒绝并记 `REQUIRED_FUNC_ERROR`。
+- 真 DuckDB:DATE/TIME/TIMESTAMP/DECIMAL 混合格式全量入库;坏值不导致整批失败。
+- 复用失败保留旧行;新表失败清理;写入异常统计 `row_unprocessed`。
+- 统计:SUCCESS/NEED_REVIEW/SKIP/FAIL/自建表的 batch、SSE、`getFiles` 数字一致。
+- 脱敏:身份证/手机号/卡号在日志和接口中掩码。
+
+## 三、Commit 2(Phase B):模板匹配提升
+
+### B1. 表头规范化与指纹
+- 括号内容保留为语义 token(`金额(元)`→`金额元`、`账号(本方)`→`账号本方`);只去装饰性标点/空白。
+- 指纹只取非空表头并编码位置(`0=姓名|2=金额`),尾部空列/padding 不影响;`headers` 改 JSON 存储(legacy 逗号 fallback)。
+
+### B2. 复用安全与案件隔离
+- AI 模板复用必须 `headerMd5` 完全相等且列序一致;否则按表头名重映射或重调模型,绝不直接套旧 colIndex。
+- 复用也跑 verify;复用前确认当前案件物理表存在,缺失则按存档字段建表。
+- 唯一键改 `(case_id_src, header_md5)`;同批次同表头按 `(case, md5)` 复用;`register` 冲突后复用/重激活 OFFLINE。
+- 列数不一致不再 `dropTable`:`ALTER TABLE ADD COLUMN` 或新模板;只删当前 file/sheet 的行。
+
+### B3. 打分与出口闸门
+- 增加字段名匹配、required 覆盖、`ColStat.shape` 类型兼容、表头顺序;修正 Jaccard 去重。
+- 同义词小词典(户名/账户名称、发生额/交易金额、交易日期/日期、账号/卡号、电话/手机号等)。
+- 阈值自适应:短模板 ≤3 列要求全中或 ≥0.9;普通模板 `STRONG≈0.85 + required 全覆盖 + shape 不矛盾`;`WEAK≈0.55` 接入。
+- 出口 A 必须确定性分数达标 + required 覆盖;模型自评只做辅助,0 分候选不能靠 0.6 自评升 A。
+
+### B4. 提示词与绑定校验
+- 候选 brief 增加 score/来源/category/表名/required/类型/字段中文名;AI 模板展示 `cN + 中文名 + 类型`。
+- 输出改稳定 `fieldId` + `fileColIndex`(明确 0 基);`filterBinds` 做 trim/大小写/下划线归一,接受中文/英文别名,检测 1-based 偏移、重复源列/目标字段。
+- 无效绑定回灌模型 repair 一次;仍失败 → NeedReview;第二次 `askLlm` 单候选重编号 #1。
+- 模型 `headerRow` 校验后使用;headWindow 改为围绕 primaryBlock 表头行。
+
+### B5. 向量召回与候选池
+- `mergeRecall` 真正补候选:召回命中不在池内的 ONLINE 模板加载字段后追加;去重;header top N + vector top M 配额。
+- 嵌入/查询文本同构;移除随机 `ai_clean_{id}`、通用 sheet 名、纯 cN;加入分类/字段中文名/类型。
+- 候选池一次 join + 缓存;跳过无字段模板;按表头/字段集合去重;过滤 `main_id` 不在 `StrategyType` 的既有模板。
+
+### B6. 探测与结构
+- CSV 走 `AiRowReader`/raw 表流式前 200 行探测,不再用 POI;CSV 不再 Skip。
+- 加密文件把 `FileInfo.password` 传入 `SheetProbe`/`AiRowReader`。
+- 多块:按 `BlockHint` 每块建 job(`block_no` 已有唯一键),每块独立 headers/colStats/md5;入库只读 `firstDataRow..lastDataRow`。
+- 合并单元格:先尝试表头压平(横向填充、纵向 `/` 合并),成功则不转人工;数据区合并不再一刀切;无法压平才 NeedReview;`BlockHint.mergedRegions` 真正装配。
+- 表头定位加入列形状/文本密度/关键词/下一行数据打分;结构门移到模型调用之前。
+
+### B7. 指标与闭环
+- 批次增加漏斗:headerCache/scoreHit/llmHit/selfBuilt/needReview/failed;`matched_by` 区分 HEADER_CACHE/SCORE/LLM/SELF_HEADER。
+- 后续:人工修正 API、DRAFT→ONLINE 晋升、负样本/golden set。
+
+### B8. 测试
+- 归一化/指纹:括号限定词、单位、尾部空列、列序、逗号列名。
+- 匹配:同义词、短模板、列序变化、低分候选不得出口 A、向量补候选、配额。
+- 复用:精确 md5、列序变化重映射、表不存在建表、同批次同 md5、跨案件隔离。
+- 探测:CSV、加密、多块、合并表头、20 行以后表头。
+- 端到端:多块/合并/CSV 实际入库;匹配漏斗计数;无错列写入。
+
+## 四、验收标准
+- **Commit 1**:上述行级诊断/计数测试全绿;真 DuckDB 上 DATE/TIME/TIMESTAMP/DECIMAL 不再整批失败;`ai_clean_reject` 能查到每一行被拒的列与原因;`row_total=row_loaded+row_rejected`(成功任务);未清洗 sheet 不再计成功。
+- **Commit 2**:golden set(同表头/同义词/列序/多列/少列/括号单位)档位符合预期;列序变化不写错列;同批次同 md5 全部入库;跨案件互不污染;CSV/多块/合并表头可入库。
+- **前后对比**:用 `smart-clean-test-cases.md` + 三个真实样本跑同一批,记录匹配漏斗、成功/失败行数、逐列错误率;以“自建表不参与匹配率、未清洗不计成功”后的真实基线为准。
+
+## 五、风险与顺序
+- 拒绝记录表会随数据量增长:按 job/file 清理 + 可配置保留上限;应用日志只采样。
+- 脱敏默认开启,避免身份证/手机号/卡号进日志。
+- 必填严格化会增加失败行,但每一条都能在 `ai_clean_reject` 中看到原因。
+- 唯一键变更需要迁移/回滚脚本;先建新索引、回填 `case_id_src`、再删旧索引。
+- 多块/合并表头会增加 job 与模型调用;加每 sheet 最大块数、每批 token 预算护栏。
+- 先提交 Commit 1 并跑完测试,再开始 Commit 2。

+ 158 - 0
.zcode/plans/plan-session_4489eda9b3f20102402e7c5a918902ee.md

@@ -0,0 +1,158 @@
+## 目标
+
+桌面端启动时先绑定后端服务地址,绑定成功且服务可达后,才进入原有的「健康检查 → 授权校验 → 登录」流程。
+
+## 总体流程
+
+```
+启动
+ ├─ 未绑定 && 构建开启绑定要求
+ │    → /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。