# 04: 配置与凭据注入 **What to build:** 通过部署环境或后端运行时配置注入后端地址、天地图、Cesium、WebSocket 等配置与凭据。前端不硬编码账号、密钥或环境地址。 Blocked by: 01 Status: resolved Labels: ready-for-agent > 请勿据标签重复接单:本票已完成并有关闭记录,`Labels` 保留只是为了让面板不把它退化成 `needs-triage`。 - [x] 配置统一由配置服务读取,页面不直接拼接环境地址。 - [x] 后端接口、天地图、Cesium、WebSocket 均可由配置覆盖。 - [x] 真实接口的 `Authorization: Bearer ` 由部署层注入(生产网关补头,开发本地反代或部署配置提供只读 token);适配层统一附加,页面与 store 不触碰凭据。 - [x] 凭据缺失时不进入登录页,而是显示数据不可用状态。 - [x] 数据源默认 mock;真实接口由部署配置显式开启,两者走同一适配层。 - [x] 配置加载成功、失败和缺失三种状态可演示。 - [x] 有配置加载纯逻辑单测。 ## 进度:100% **已完成并关闭(2026-09-17)。** 后端地址、天地图、Cesium、WebSocket 与凭据都收进配置服务,前端零硬编码;默认 mock,真接口要部署配置显式打开;凭据只在适配层出现一次。 ### 与维护者确认的两条前置决策 - **注入方式:运行时分层**(后者覆盖前者,深合并):内置默认 → 构建期 `VITE_COCKPIT_*` → `cockpit/config.json` → 宿主注入 `globalThis.__COCKPIT_CONFIG__`。 - **联调凭据**:仓库只放 `public/cockpit/config.example.json`;真实 `config.json` 进 `.gitignore`;本机只读令牌写进它或 `.env.local`(也已 gitignore),生产一律由网关补头。 ### 改动 - 新增 `domain/configStatus.js`:配置/数据源/凭据词汇 + 「状态 → 色键/文案」纯函数映射;另给票 05 留了 `isCredentialFailure()` 与统一文案 `DATA_UNAVAILABLE_LABEL`。 - 新增 `services/cockpitConfig.js`:四层解析、宽松校验(错字段退回默认并记 issue,不整份作废)、三态 `ready/missing/failed`、同步读 `getCockpitConfig()`;**没有任何配置来源时一个请求都不发**(原因见下方「一个坑」)。 - 新增 `services/cockpitApi.js`:适配层接缝。`planCockpitRequest()` 是全仓唯一拼地址、唯一附加 `Authorization` 的地方(且只在 real 分支加);`requestCockpitJson()` 是 mock/real 唯一出口,失败一律收敛成 `{ok:false, reason}`,不抛异常、不跳登录页;mock 处理器注册表留给票 05 挂领域样例。 - 新增 `public/cockpit/config.example.json`(部署模板);`.gitignore` 忽略 `public/cockpit/config.json`;`public/cockpit/README.md` 补「部署配置」一节:四层、三态、凭据策略、**部署后怎么改无需重新构建**。 - 外壳顶栏加配置标签(复用票 06 的 `StatusTag`):`data-config-state` / `data-data-source` / `data-source` 供走查断言;界面只显示状态,不显示任何令牌。 - 配置真被消费,不是摆设:`map.cesium.enabled=false` → 地图服务不启动引擎并如实标注 `disabled`(中央区域留临时影像兜底);`map.cesium.baseUrl` → 覆盖 `CESIUM_BASE_URL`(`cesiumAssets.ensureCesiumBaseUrl(override)`)。 - 未动 `MapCanvas`、面板与 store:它们连配置服务都不直接 import,页面只读外壳给的视图模型。 ### 验证证据 - **纯逻辑**:`node .scratch/cockpit-shell-panel-layout/verify/t04-config.mjs` → 通过。覆盖三态、四层来源与覆盖顺序、宽松校验、载荷判定、请求计划、mock/real 同一出口、假 401 归类;含两条**静态守卫**:8 个页面/store 文件零 `Authorization`/凭据字样、15 个模块文件零登录跳转,且 `VITE_COCKPIT_*`/`__COCKPIT_CONFIG__` 只在配置服务里读。 - **真机走查**:`node .scratch/cockpit-shell-panel-layout/verify/t04-config-walkthrough.mjs` → **9/9**:默认缺失态(内置 mock、无页面报错)、宿主注入成功态(real)、运行时文件成功态(`source=file`,真走 HTTP)、坏文件失败态(红色「数据不可用」、URL 不变、页面里没有登录入口)、`cesium.enabled=false` 生效;并断言**注入的令牌不出现在 DOM/顶栏**。 - **回归**:`t01-browser-walkthrough` **20/20**(A11「无页面级报错」保持绿)、`t01-panel-catalog` / `t03-camera-contract` / `t06-assets` / `t28-fill-container` 全通过、`yarn build` **exit 0(55s)**。 - **归因(对抗式)**:`t03-camera-walkthrough` 有 2 项红(A5/A8:中心点投影)。把本票三个改动文件 `git stash` 回 HEAD 复跑,**同样两条、同样数字**(投影 827,417 vs 画布中心 840,520)→ 属票 03 在途工作(claimed / 90%),与本票无关;随后已 `stash pop` 还原。 ### 一个坑(值得记住) dev 下 Vite 的 HTML 回退只对 `Accept: text/html` 生效;配置服务发的是 `Accept: application/json`,所以「没有配置文件」拿到的是**真 404**(Chrome 记一条 console error),不是 200 HTML。第一版按「默认总去探测 `cockpit/config.json`」实现,实测把 `t01` 的 A11 打红;改为**没有显式配置 URL 就不发请求**(此时 `missing` 表示「没有任何配置来源」),文件层由部署侧显式开启,三种等价写法:宿主页一行 `window.__COCKPIT_CONFIG_URL__ = 'auto'`、构建期 `VITE_COCKPIT_CONFIG_URL=auto`、或直接给绝对 URL 从后端拉(`auto` = 按应用基址取约定路径,qiankun / 子目录部署下也对)。README 已写清。 ### 边界与遗留 - 天地图与 WebSocket 目前只有配置位与文档化的消费点(`map.tianditu.token` → 票 21;`ws.url`/`ws.token` → 票 15);本票保证它们**能被配置覆盖**,真正使用落在那两票。 - 「401/凭据类失败只把**分区**标成数据不可用」的渲染归票 05(其验收原句);本票已提供 `REQUEST_REASON.UNAUTHORIZED`、`isCredentialFailure()`、统一文案,并用假 401 在逻辑脚本里断言过。 - 为放部署模板,票 06 的 `t06-assets.mjs` E1「public/cockpit 下每个资源都有引用点」需把 `*.example.json` 与 README 一视同仁(模板由运维复制,代码永不引用):已按此最小修改,`aerial.webp` 的引用清单照旧。 - 本票改动**未提交**(工作区里另有票 03 会话的提交 `71c55bc` 与在途改动)。