---
name: microi-microservice
description: Microi 前端微服务 MicroService 开发与交付指南。用于创建、读取、修改、构建、发布或修复 Vue3 微应用，识别线上 ApiBase/OsClient 并用本地 61500 隔离复现，维护 microi.routes.json，绑定 sys_menu，使用 V8.OpenAppDialog，处理菜单页签 keep-alive、appstate-change、状态保留、滚动条、骨架屏或白屏，或通过 MCP 管理 Web、UniApp、MicroService 应用源码和运行时。
---

> **Codex 非阻塞自动更新：** 当前宿主为 Codex 时，吾码 CLI、Codex 插件与工作区 AI/MCP 由后台自动更新；需要诊断时读取 `../microi-codex-installer/SKILL.md`。更新失败、等待空闲或尚未重载均不得阻断当前、正在进行或新建任务。非 Codex 宿主跳过此项。

# Microi 前端微服务

这里的 MicroService 是运行在吾码主站中的前端微应用，不是 .NET/Java 后端微服务。
它适合复杂租户页面、多页面应用、完整表格/上传/步骤交互和独立版本交付；业务事务、
权限和最终校验仍放接口引擎或可信后端。

完整数据模型、文件/路由协议和宿主通信见 `references/runtime-delivery.md`。

## 何时使用

| 需求 | 选择 |
|---|---|
| 简单确认 | `V8.ConfirmTips` |
| 标准单表新增/编辑/查看 | FormEngine / `V8.OpenAnyForm` |
| 主前端已注册组件 | `V8.OpenDialog` |
| 3 个以上字段、联动、上传、表格、Tab、步骤条、代码编辑 | MicroService + `V8.OpenAppDialog` |
| 复杂区域需要固定嵌入表单、独立发布且不进入主前端 | `DevComponent` + MicroService 路由 |
| 独立菜单、多页面、AI 在线编辑、本地 Vite、独立发布 | MicroService |

## 默认发现顺序

开始写代码前：

1. `microi_list_applications` 盘点当前租户全部 Web、UniApp、MicroService 和文件清单。
2. `microi_get_application_context` 默认只读取元数据、文件哈希、运行时和页面；需要少量正文时显式开启内容读取。
3. 单个大文件按需使用 `microi_get_application_file`，不要为了查看清单把全量源码 Base64 拉入上下文。
4. 合适应用存在时在其中新增页面/路由，不创建“一页一个微服务”。
5. 没有合适应用时才脚手架/创建新 AppKey。

所有读写必须使用同一 MCP、ApiBase 和 OsClient。写操作先 dry-run，再按用户授权确认。

## 源码与产物分离

- `sys_microistore`：应用主数据。
- `mci_ai_app_file`：私有源码清单，内容在私有 HDFS。
- `mci_ai_app_version`：构建版本。
- `sys_microiservice`：已发布运行时。
- `sys_microiservice_page`：页面路由。
- 编译后的 HTML/JS/CSS/图片放公有 HDFS，源码不公开。

不能从 `sys_microiservice` 公有产物反推完整源码，也不把大 JS/CSS 长期塞数据库 JSON。

## 本地工程

新建和整体升级项目的通用前端架构遵守 `microi-ai-application`：默认使用 Vue 3 单文件组件、Composition API、Vite 和严格 TypeScript。本 Skill 继续负责 MicroService 特有的 Manifest、页面路由、宿主上下文、菜单绑定与发布协议。

项目至少有：

```text
.microi-micro-app.json
microi.routes.json
package.json
package-lock.json
tsconfig.json
vite.config.ts
index.html
src/
```

AppKey 稳定且只含安全字符。`microi.routes.json` 是页面事实源，删除/新增路由后由
发布流程同步 `sys_microiservice_page`，不要从 Vue 源码猜路由。

普通本地项目必须位于当前连接对应的 `Microi-V8-Engine/{系统名称} ({ApiBase域名})/{OsClient}.{OsClientType}.{OsClientNetwork}/AI应用/{appKey}`。该应用目录同时承载 MicroService 源码、Manifest、接口引擎、资源策略、测试和应用商城上传素材，是唯一事实源。受审计的官方内置应用若由独立 Git 仓库维护，必须有版本管理的发布契约唯一指向该源码根，所有构建、跨工程测试和发行包都读取契约；同名租户同步目录只能是只读镜像。禁止自动在多份副本中择新、创建平行 `microi.apps/` 发行根，或在应用内嵌套第二份可编辑 `microservice/` 工程。

构建前遵守本地 OOM 保护；已有 dev server 可复用时不重复启动。新脚手架必须支持独立
访问时的平台帐号登录，但独立 Vite 预览仍没有菜单/弹窗等完整宿主上下文，不能替代宿主验收。

- Vite 微服务的 `package.json.scripts.dev` 必须存在且可直接启动，标准值为 `vite --host 0.0.0.0`。Microi VS Code 在创建、拉取、打开、保存项目文件、构建和推送前都要检查：缺失且能由 `vite.config.*`、Vite 依赖或既有构建脚本确认是 Vite 项目时幂等补齐；已有自定义 `dev` 命令必须原样保留；构建体系无法确认、`package.json` 非法或顶层不是对象时失败关闭，禁止猜测并改坏其它框架。

### 线上目标在本地 Microi.Client 复现（强制）

- 先在一次性隔离 browser context 打开线上友好路由并读取
  `window.__MICROI_RUNTIME_ENDPOINT__`；旧版回退读取页面全局、同源缓存和域名解析，禁止猜
  ApiBase/OsClient。
- 在新的独立 context 打开
  `http://localhost:61500/?OsClient=<encoded>&ApiBase=<encoded>#/micro-app/{MsKey}/{RoutePath}`。
  两个 URL 参数是当前页面最高优先级；初始化后回读同一个运行时对象确认命中目标。
- 同源普通窗口共享 Token、CurrentUser、ApiBase、OsClient。并行不同目标必须一组目标一个
  Playwright `browser.newContext()` 或独立浏览器 Profile；多个无痕窗口不保证彼此隔离。
- 本地复现、主框架宿主验收、远端运行产物和生产部署分别报告；本地通过不能替代发布后回读。
- 新租户或历史还原库可能没有 `SysConfig.ApiBase`。微服务安装不得依赖该字段才能写入运行 HTML：按目标 `OsClient` 注入上下文，运行时使用同租户宿主的 `microApp.getData()` 或绑定该租户的 API 稳定入口，保留代理路径；不得沿用发布端上下文、跨租户宿主或把 HDFS/CDN 域名当成 API。独立 Web/UniApp 仍需有效的目标 API 配置。
- 登录用户的角色等历史字段可能返回数组、JSON 字符串或空值。列表运算前必须归一化类型，并优先使用当前接口给出的结构化字段；验收应使用真实旧格式数据验证页面渲染及导航，不能只断言接口成功或没有 `pageerror`，还应检查 Vue 捕获到的控制台异常。

迁移 Vue2 定制页到独立 Vite 微服务时，不得假设宿主会提供 Tailwind/UnoCSS 等原子类；
页面依赖的宽高、颜色、间距、响应式和打印/下载样式必须由组件自身的语义 class 明确声明，
并检查最终 CSS 产物确实包含这些规则。合并多个客户分支的同名组件前逐份比对功能，选择
业务能力超集并保留各分支共同的数据契约；不能仅按文件名去重，否则会静默丢失某一分支的
筛选、确认、导入参数或状态同步。验收需用旧版截图逐项核对标题、工具栏、选项、按钮、表格
和分页，而不只确认组件已挂载。

### 本地源码同步禁止人工分段

- 已有本地工程时，`microi_sync_microservice_source` 首选只传项目绝对路径 `directory`；先 dry-run 审阅文件数、总大小、逐文件哈希和清单哈希，再传 `confirmExecution`。
- 源码扫描、读取、哈希和上传内容组装必须在 MCP 进程内完成，模型上下文只接触清单。禁止让 AI 读取整个源码为 Base64，禁止生成 `.sync-seg-*`、`sync-source-files.json`，禁止把一个真实源码文件拆成多个临时文件反复调用工具。
- 目录扫描必须排除 `node_modules`、`dist`、`build`、`coverage`、缓存、版本库和 UniApp 构建目录；发现 `.env`、证书、私钥、符号链接或越过根目录时失败关闭。
- AI 工具单次读取上限不是 MicroService 源码文件上限。即使 `microi.v8.js` 等文件超过 50KB，也应保持一个完整文件，由 MCP 直接从磁盘读取。
- `sourceFiles` 仅用于调用方本来就持有内存文件的旧版兼容场景；不得把它作为本地工程默认路径，也不得用人工切片规避上下文限制。

### 多人协作发布前远端合并门禁（强制）

- 每一次同步或发布（包括同版本重试、紧急修复和仅编译产物发布）开始前，必须重新调用 `microi_get_application_context` 拉取目标租户当前完整私有源码清单、正文、版本与逐文件哈希；禁止沿用任务开始时或上一次发布后的旧快照。
- 以最近一次已确认同步的源码清单为 Base，对 Base、本地 Local、远端 Remote 做逐文件三方比较：仅远端变化先拉取，仅本地变化保留本地，双方变化必须按真实业务语义合并并重新构建测试；删除/重命名同样纳入冲突判断。没有可靠 Base、远端源码不完整或冲突未解决时失败关闭，不能把时间戳较新当作自动胜出规则。
- `replace`、`ReplacePrivateSourceOnly` 和流式发布都不是覆盖他人代码的授权。发现远端版本、文件正文或哈希晚于本地基线时，禁止直接用本地目录整包覆盖；必须先完成上述合并，并让本地唯一事实源保存合并结果。
- 合并完成后冻结 `expectedCurrentVersion`、`expectedAppVersion`、`SourceManifestHash`、`RuntimeManifestHash` 与 `DeliveryBatchId`。在源码同步、stage、finalize 任一步之前再次回读；任一版本或哈希漂移都说明期间有人发布，必须废弃当前 stage，重新拉取、三方合并、构建和预检。
- 发布后必须逐文件回读远端私有源码与公有运行产物并核对哈希，再把该远端清单保存为下一次三方比较的共同 Base。只回读入口 `200`、只核对版本号或只保留本地构建目录，都不能证明多人协作发布没有丢代码。
- `CurrentVersion`、`AppVersion`、文件时间和语义版本字符串都不能单独决定谁是“新代码”；它们只作为 CAS 世代的一部分。最终事实必须同时满足三方文件合并、期望远端源码清单哈希和服务端应用锁内 CAS，任何一个不一致都拒绝切换。

## 发布

- 创建/更新元数据：`microi_create_microservice`。
- 同步私有源码：`microi_sync_microservice_source`；本地工程必须优先传 `directory`，不构造 Base64 文件数组。
- 真实编译目录优先 `microi_publish_application_directory_stream` 流式发布。
- `changeSummary` 是最多 2000 字符的版本说明文本，不能传对象；v3 在首次 stage 创建版本时存入 `mci_ai_app_version.ChangeSummary`，后续同请求重放和恢复保留原说明。历史节点若发布完成但摘要仍为空，应报告后端版本缺口，不得把编译文件验证成功等同于日志验收通过。
- 每次创建、修改、升级或重新发布微服务，必须先为目标精确 `AppVersion` 在 `sys_microistore_changelog` 写入完整日志，再同步源码、stage、finalize 或制作商城包。日志必须关联真实 `StoreId`，包含 `Title / ChangeType / Content / ReleaseTime`；流式发布显式传入与日志含义一致的非空 `changeSummary`。发布后同时回读商城日志、`mci_ai_app_version.ChangeSummary` 与包内 `PackageInfo.ChangeLog`；缺失或版本不一致必须失败关闭。
- 发布动作必须明确区分两种模式：默认“源码+编译产物”先把完整工程同步到私有桶并逐文件回读 SHA-256，再把 `dist` 流式发布到公有桶；显式“仅编译产物”只更新公有桶，必须在界面中告知其他用户仍会拉取上一次私有源码，禁止暗示源码已同步。
- 私有源码同步使用 `ReplacePrivateSourceOnly` 精确清理过期源码；兼容调用可以继续接受 `replace`，但实现不得用旧式全表 `Replace=true` 删除同一应用的公有运行产物元数据。
- 私有源码正常通道使用 `StageMicroServiceSourceFile`：每个普通文件以 multipart 流从磁盘上传，同一 `DeliveryBatchId + RelativePath + SHA-256` 的暂存重试必须幂等；全部完成后由 `FinalizeMicroServiceSourceManifest` 在应用锁内核对 `ExpectedCurrentVersion / ExpectedAppVersion / ExpectedSourceManifestHash`，逐文件从私有 HDFS 回读大小与 SHA-256，再一次切换 `PrivateSourceStaged → Private` 并归档旧清单。禁止把源码复用到公有 `UploadApplicationAssetStream` 路径。
- `write EPIPE`、超时或连接关闭发生在 stage 时，只能对上述幂等暂存单元作有界重试；发生在 legacy 整包请求或 finalize 时提交结果未知，先按完整路径集合、字节数和 SHA-256 回读，完全一致才能按成功恢复，否则停止并要求重新三方检查，禁止盲目重放非幂等请求。
- 旧服务器没有私有源码流式能力时，只允许小于等于 8 MiB 的源码使用单 JSON Base64 兼容接口，并且 HTTP 请求体仍必须 64 KiB 分块、遵守 socket backpressure。超过上限必须明确要求升级后端，禁止为了“避免 EPIPE”人工拆源码文件、循环部分覆盖或调大代理请求体上限。
- `mci_ai_app_file` 同时存在私有源码和公有编译产物。源码拉取/差异比较必须优先按 `StorageScope=PublicBuildStream|PublicBuildStreamArchived|PublicBuildOnly` 排除公有产物，并保留旧数据中 `HdfsPath == PublishHdfsPath` 的兼容判断；不能仅靠两个路径相等识别，否则版本路径与稳定别名不同的流式产物会被误读成私有源码。
- 只有服务器不支持流式端点且产物很小时，才兼容 `microi_publish_microservice`。
- 正常发布支持最多 20,000 个文件、总计 20GB；逐文件从磁盘流入 HDFS，不生成整包 Buffer/Base64。几百 MB、1GB 级项目不得自动降级到旧 Base64 发布器。
- 普通应用的 `StorageMode=db` 仅是显式的小型应急恢复模式，不是正常发布容量；当前最多 256 文件/5MB。超过该边界必须修复租户 HDFS/网关并恢复流式文件模式，不能调大数据库内联上限来承载大项目。
- 受信任的平台启动应用可把 `Source=NotIncluded + Build=DatabaseOnly + StorageMode=db` 作为长期启动/恢复介质，但只能携带已校验编译产物，仍受 256 文件/5MB 限制，不得携带可编辑源码或租户秘密。其数据库产物是离线权威；HDFS/CDN 只能作为逐文件回读哈希一致后的镜像，镜像故障或漂移时继续使用数据库产物。
- 每次交付使用同一 `DeliveryBatchId`，并保存 `SourceManifestHash` 与 `RuntimeManifestHash`；源码同步、运行清单、页面切换和入口探测必须能关联到同一批次。官方内置应用还必须在发布前核对唯一源码版本、源码清单、`dist`、路由与所有内置包，任何一处哈希漂移都失败关闭。
- 发布后用 `microi_get_application_context`、`microi_get_microservice` 回读。
- 回读成功还不等于页面可用：必须直接请求稳定入口、版本入口和清单内的 JS/CSS；入口 `502` 且运行时/清单存在时，优先检查 API 节点是否能通过租户 MinIO 内网端点读取公有桶对象。不要反复发布同一份产物掩盖存储读取故障。
- 服务器暂未部署修复且普通应用产物很小时，可把 `StorageMode=db` 与内联 `ContentBase64` 作为短期恢复手段；必须明确记录为临时方案。修复部署后重新流式发布到公有 HDFS，并恢复 `StorageMode=file`。只有上一条满足严格资源策略的受信任平台启动应用允许长期保留小型数据库产物；禁止借此把普通或大体积 JS/CSS 长期放进数据库 JSON。
- 子租户发布时，源码、版本资产、回读验签、页面与缓存全部绑定当前 Token 的 `OsClient`；禁止回退到主租户或宿主服务器默认租户。切换前必须由当前 API 节点通过该子租户 HDFS 配置读回每个版本文件并校验大小/SHA-256。

## 菜单、弹窗与表单嵌入

菜单 `OpenType=MicroService` 时一次绑定 `MicroServiceId`、
`MicroServicePageId`、`MicroServiceRoutePath`、`MicroServiceKey`。
完整系统 Manifest 不得固化不同租户会变化的两个 Id；模块声明 `openType=MicroService`、`microServiceKey` 和 `microServiceRoutePath` 即可，由 `microi_generate_system` 在任何写入前回读 `sys_microiservice/sys_microiservice_page`，解析并校验当前租户的 `MicroServiceId/MicroServicePageId`。微服务或页面不存在时必须在首个写操作前失败，不能留下半套系统。
复杂弹窗用 `V8.OpenAppDialog`，业务参数放 `Data`，回调放顶层。

### 独立、菜单、弹层与表单嵌入四种入口

- 同一发布物必须支持：直接打开独立运行、`sys_menu` 使用 `/micro-app/{AppKey}/{RoutePath}` 打开指定路由、`V8.OpenAppDialog` 按 AppKey/RoutePath 以 Dialog 或 Drawer 打开、表单 `DevComponent` 按组件路径别名嵌入指定 RoutePath。
- 表单嵌入时，在 `microi.routes.json` 的目标页面声明稳定、唯一的 `LegacyComponentPaths`；字段 `Config.DevComponentPath` 与其归一化后匹配。主前端存在同路径 Vue 文件时本地组件优先；不存在时 `DynamicComponentCache` 才交给 MicroService 组件宿主。新项目使用不与 `/src/views` 真实文件冲突的虚拟路径，历史字段无需逐租户改配置。
- 组件宿主必须下发 `componentMode=true`、可序列化的 `componentData`、当前路由和权限上下文；子应用用 `dev-component:resize` 同步 80～1600px 高度，用 `dev-component:event` 回传 `update:modelValue`、`CallbackFormValueChange`、`FormSet` 或 `ParentFormSet`。禁止依赖父页面 DOM、Vue 实例、函数或 `ParentV8` 跨 iframe 传递。
- 菜单路由必须同时回读并传入 `SysMenuId`、`ModuleEngineKey`、`DiyTableId`；弹层与表单组件默认继承调用菜单，也允许跨模块时显式传真实授权模块。宿主统一下发 `{ sysMenuId, moduleEngineKey, diyTableId }` 的 `permissionContext`。
- `permissionContext` 只是选择正确 API 调用上下文，不是授权凭证。后端仍依据 DiyToken、OsClient、角色、菜单、表、按钮和数据范围校验；禁止删除权限参数、改成匿名接口或写死管理员 Token 来消除“没权限”。

### 独立运行的认证门

- 嵌入菜单、表单定制组件或 `V8.OpenAppDialog` 时直接复用宿主 Token，不显示第二套登录页；同一个 V8 SDK 实例继续处理 Token 轮换。
- 独立访问时先配置当前 `apiBase/osClient` 并复用本地有效 Token；没有有效 Token 才显示吾码帐号密码登录。
- 页面启动时调用 `V8.GetSysConfig(true)`，用统一 `isEnabledFlag` 解析 `EnableCaptcha`。开启时请求 `GET /api/Captcha/GetCaptcha?OsClient=...`、读取响应头 `captchaid`，登录提交 `_CaptchaId/_CaptchaValue`；关闭时不渲染、不提交验证码字段。
- 登录使用统一 `V8.Login` 与 DiyToken，不创建第二套用户体系。Token 失效后回到认证门；禁止在 URL、日志、源码、`.env` 或业务数据中保存 Token。
- “无权限”排查顺序固定为：Token/OsClient → 目标 `ModuleEngineKey` → 宿主 `permissionContext` → 当前角色的菜单/表/按钮/数据范围。登录成功不等于拥有全部模块权限。

微服务内部禁止调用浏览器原生 `alert/confirm/prompt`。优先复用宿主 `Tips`/`V8.ConfirmTips`；需要由子应用自行承载时，使用 teleport 到 `body` 的品牌化可访问弹层，固定在当前视口正中央并高于宿主滚动内容。长列表只允许一次性加载后在前端内存搜索时，不得随着关键词重复请求服务器。

Token 只通过宿主上下文传递，不硬编码、不放 URL、不写日志。子应用回传成功/取消/
错误事件，宿主负责提示、关闭和刷新。

菜单型微服务通过 `window.microApp.getData().hostCapabilities` 发现主框架能力，禁止直接操作
父页面 DOM、Pinia 或 Vue Router。能力协议固定为 `microi.host.v1`，请求使用：

```js
window.microApp.dispatch({
  type: 'micro-app:host-action',
  action: 'closeTab',
  requestId: 'optional-id',
  data: {}
});
```

AI 生成菜单微服务时，应优先封装一个 `callMicroiHost(action, data)`，先检查
`hostCapabilities.actions`，再 dispatch。当前标准动作是：`closeTab`、`navigate`、
`replaceTab`、`back`、`forward`、`reloadTab`、`setTabTitle`、`showMessage`、`setGlobalOverlay`。
`setGlobalOverlay` 只负责平台级遮罩、宿主滚动锁和必要时提升微应用层级，不替代子应用自己的 Dialog、焦点管理和权限。平台级详情/确认弹层打开时传 `visible/blur/lockScroll/mask/promote`，关闭、路由离开、错误和 `onBeforeUnmount` 都必须发送 `visible:false, lockScroll:false, promote:false`；多个弹层用本地计数或统一 computed 保证最后一个关闭后才撤销。
`navigate/replaceTab` 只传以 `/` 开头的站内 path 或 `{name,params,query,hash}`；禁止传
外部 URL、登录页、访问密钥页或内部 redirect。目标仍要存在于当前用户动态路由并经过路由守卫，
宿主桥接不授予菜单或数据权限。业务保存成功后才能关闭/跳转，不能把尽力返回的
`micro-app:host-action-result` 当作业务持久化确认。

微服务需要平台普通打印时使用 `openPlatformPrint`，并先确认它存在于
`hostCapabilities.actions`。参数只传 `mic_print.Id`、标题和当前 `apiBase` 同源且以
`/apiengine/` 开头的数据地址；地址必须明确携带与页面一致的 `OsClient`，不得携带 Token、
帐号密码或其它 URL 凭据。该动作只表示 Print Engine 预览已打开，不代表浏览器已打印或
打印机已出纸，也不是 BLE/SPP 蓝牙代理。独立运行没有宿主时应隐藏/禁用入口。可复制示例见
[`references/runtime-delivery.md`](references/runtime-delivery.md#微服务调用平台普通打印)。

微服务自己的左侧菜单、页签和详情层级属于**子应用内部路由**，必须由子应用 Vue Router、
状态机或 iframe 内 Hash 管理；禁止为 `/overview`、`/portal` 等同一 AppKey 页面调用宿主
`navigate/replaceTab`。吾码 TagsView 以主框架 `$route.fullPath` 作为组件 key，修改宿主路由会
卸载并重新挂载整个微服务。内部页面较多时使用 `defineAsyncComponent`，并把 `Suspense` 骨架屏
放在右侧内容容器内，使微服务导航栏和顶部栏保持挂载；浏览器 `popstate/hashchange` 要能恢复
内部路由。只有确实要离开当前微服务、打开另一个吾码后台菜单或替换顶部 Tab 时才调用宿主路由动作。

### 页面打开与路由切换骨架屏（强制）

所有新建或修改的 MicroService，在首次打开、内部路由切换、菜单页签冷启动以及影响主要内容几何的
异步数据读取期间，都必须先同步呈现骨架屏；禁止只显示转圈图标、纯文字“加载中”、整页
`v-loading` 遮罩、空白画布或等待接口结束后才补 Loading。顶部栏、子应用导航和不依赖新数据的
操作保持挂载，骨架只接管正在变化的内容容器。

- 路由组件使用 `defineAsyncComponent + Suspense fallback`；已缓存组件再次切换也要有短暂且可观测的路由骨架，组件挂载后的接口加载继续使用页面内骨架，避免两者之间闪白。
- 骨架必须复刻最终页面的主要几何：列表保留工具栏、行和分页轮廓，表单保留分组与字段轮廓，设计器保留节点库/画布/属性栏，3D、图表和大屏保留画布边界、图例及主体轮廓。不能用一组与最终布局无关的灰条冒充。
- 加载容器必须设置 `aria-busy="true"` 并提供可读状态文本；`prefers-reduced-motion: reduce` 时停用 shimmer。成功、空数据、失败和无权限是四种独立终态，失败不得永久停留在骨架。
- 骨架必须在 import/fetch 之前进入 DOM，切换期间不销毁宿主导航；真实浏览器验收要用网络节流或可控延迟分别截取首次打开和至少一次内部路由切换的骨架，再确认最终页面、前进/后退与错误重试均正常。

复盘：AI 工作流业务蓝图曾在路由切换后先显示空白/转圈，根因是只给异步组件配置了加载逻辑，
没有把路由加载、页面数据加载和最终几何统一成状态协议。通用规则是“路由 fallback + 页面内骨架 +
明确终态”三层同时存在，并以真实浏览器慢网截图作为交付门禁。

### 菜单页签缓存与子应用生命周期（强制）

- 菜单微服务只有一个缓存所有者：Vue 路由宿主固定 `meta.keepAlive=false`，`<micro-app keep-alive>` 独占子应用状态。禁止把外层 Vue `KeepAlive` 打开，也禁止子应用通过随机实例名规避运行时缓存；双层缓存会产生旧宿主与当前路由竞争、无数据、永久骨架屏和白屏。
- 每个主框架 `fullPath` 使用稳定且不泄露查询参数/Token 的实例指纹。平台最多保留 5 个菜单运行时，超过后按 LRU 销毁最久未使用的隐藏实例；Tab 仍保留，再次进入允许冷启动。关闭当前/其它/全部 Tab、访问记录淘汰、退出登录、Token 重置、角色变化、同路由版本或入口变化时必须精确 `unmountApp(name,{destroy:true,clearData:true})`。
- 恢复隐藏实例时，宿主用 `forceSetData` 同步当前 Token、OsClient、权限、主题、路由和视口，再复核 `micro-app-body/#app` 的真实可见 DOM；失败只允许自动销毁重建一次。`hostCapabilities.lifecycle` 暴露 `cacheOwner=micro-app`、`cacheMode=runtime-keep-alive`、`maxCachedTabs=5` 和状态事件。
- AI 创建或修改菜单微服务时必须生成 `appstate-change` 适配：`afterhidden` 幂等暂停轮询、WebSocket、观察器和昂贵任务；`aftershow` 重新读取宿主数据、幂等恢复任务并在下一帧重算图表/虚拟列表。隐藏时保留表单输入、筛选、滚动与内部路由，禁止清空业务状态；弹窗和表单嵌入不套用菜单页签保活。

```js
window.addEventListener('appstate-change', (event) => {
  if (event.detail?.appState === 'afterhidden') pauseBackgroundWork();
  if (event.detail?.appState === 'aftershow') {
    configureMicroiV8(getMicroiContext());
    resumeBackgroundWorkOnce();
    requestAnimationFrame(resizeChartsAndVirtualLists);
  }
});
```

微服务所有主题变量、reset、通用元素规则必须限定在 AppKey 唯一根容器（推荐
`[data-mci-ui-root="{AppKey}"]`）下，不能只用宿主也会命中的裸 `[data-mci-ui-root]`；禁止用
`:root/html/body/#app`、裸 `*`、裸 `button/input` 污染宿主。
背景网格、光晕等全屏装饰在微服务根节点内使用 `position:absolute`，禁止
`position:fixed; inset:0` 越界覆盖吾码 Logo 与主菜单。宿主使用 `contain: layout paint` 和
`isolation:isolate` 作为第二道边界，但不能代替子应用命名空间。验收需检查宿主 Logo/菜单样式、
内部导航不重挂微服务、内容区局部骨架屏，以及前进/后退恢复。

### 平台主题、明暗模式与租户主色（强制）

所有新建或修改的 MicroService 都必须跟随宿主平台主题，不能交付只适配固定白底、固定深色或
单一品牌色的页面。宿主下发的 `themeMode`、`themeColor`、`themePalette`、`themeOnPrimary`、
`themePrimaryText`、`themeColorStrong` 与 `themeTokens` 是嵌入运行时的主题事实源；首次挂载、
`addDataListener` 收到 `host:theme`，以及 `appstate-change=aftershow` 时都要重新应用。嵌入态以
宿主值优先，独立预览才允许使用已保存偏好或 `prefers-color-scheme` 作为回退。

- 唯一根容器必须同时写入 `data-mci-ui-root="{AppKey}"`、`data-theme`、`data-mci-palette`，并把宿主字段映射为本应用的语义 Token；禁止把宿主页面的 `html/body` 当作子应用样式开关。
- 主操作色优先使用 `--mci-color-primary` / `--el-color-primary` 或宿主 `themeColor`；表面、文字、边框分别使用 `themeTokens.surface/surfaceSoft/textPrimary/textSecondary/border`。成功、警告、错误色可以保持语义独立，但必须分别提供浅色与深色可读状态。
- 表格、表单、弹窗、上传、骨架屏、空状态、错误态、禁用态、悬停和焦点环都必须覆盖浅色与深色；大面积背景不能直接写死 `#fff` 或某个深色，主题色不能只改按钮而其它视觉仍留在旧配色。
- 主题切换必须即时生效且不重挂应用、不丢失表单/筛选/内部路由；卸载时清理本应用创建的媒体查询、数据与生命周期监听器。
- 自动化验收至少覆盖“浅色 + 深色 + 一个非默认租户主题色”，同时断言根属性、计算后的主色/表面/文字、基本对比度和宿主 Logo/菜单未被污染，并各保留一张真实宿主截图。只测独立 Vite 预览、只检查 CSS 文本或只显示切换按钮均不算完成。

`closeTab` 与 TagsView 当前页签关闭语义一致，固定页签和最后一个页签拒绝关闭；顶部 Tab
右键刷新与 `reloadTab` 都应重载当前微服务。`OpenAppDialog` 页面不使用 Tab 动作，继续发送
`app-dialog:success/cancel/error` 关闭弹窗并回传结果。

`sys_microiservice_page` 是友好路由的页面事实源，`sys_menu` 只负责导航和角色权限。
无需出现在导航中的按钮页/详情页应在页面元数据设置 `InternalOnly=true`，不创建伪隐藏菜单。

宿主必须提供 `--micro-app-available-width`、`--micro-app-available-height`、
`--micro-app-safe-area-bottom`，并用 `ResizeObserver`/`visualViewport` 同步 `host:resize`。
子应用根容器使用 `min-height: var(--micro-app-available-height, 100vh)`；`100vh` 只能作为脱离
吾码宿主独立预览时的回退值。不得直接写死 `min-height: 100vh`、`calc(100vh - 100px)` 等
只适配浏览器视口或某个主站布局的高度，否则嵌入 TagsView、弹窗或移动端时会被裁剪或产生双滚动。

菜单型微服务必须只有一个纵向滚动所有者：宿主外层隐藏溢出，`<micro-app>` 边界提供默认
`overflow-y:auto` 兜底。普通长页面保持自然高度即可；子应用若需要 sticky 工具栏、虚拟列表等
内部滚动，则自己的滚动容器必须具有基于 `--micro-app-available-height` 的确定高度和
`overflow-y:auto`。真实子滚动容器会把内容约束在边界内，框架兜底条自动不出现；只给自动高度
根节点写 `overflow:auto` 不算自滚动容器。验收需分别覆盖框架兜底和子应用自滚动，断言任一时刻
只有一个 `scrollHeight > clientHeight` 的纵向滚动所有者。

相机扫描、收银称台、扫码、签到等高频工具页必须另设“一屏操作”断点：在宿主下发的可用高度内同时显示输入/取景、主操作、处理状态、核心结果和业务要求的最近记录入口，完成主流程不得依赖页面纵向滚动；竖屏优先上下紧凑分区，短横屏切换左右或多列分区。历史明细可在自己的卡片内滚动，但不能隐藏错误、当前结果、暂停/继续/恢复等关键动作。桌面应占满宿主可用宽度，通过有界预览容器和 `object-fit` 防止上传竖图把页面撑高。真实浏览器验收至少覆盖一个常见手机竖屏和一个短横屏，并断言根容器不溢出、取景区/结果区/关键历史入口同时在视口内、平台底部菜单与安全区不遮挡按钮；桌面断点必须回归，不能为移动端压缩破坏 PC 布局。

摄像头微服务必须把媒体异步生命周期当作竞态处理：每次打开、关闭和卸载都递增操作令牌或取消版本；`getUserMedia`、设置 `srcObject`、`video.play()` 每个异步边界返回后都要验证令牌仍有效。关闭时先同步切换 UI 状态并使旧操作失效，再停止全部轨道和清空 `srcObject`，防止旧 Promise 把已关闭状态反写为开启。条件渲染或点击后会移动的关闭按钮可在 `pointerdown` 捕获阶段完成停止，再短时去重后续 `click`。自动识别可以按画面指纹去重，但用户点击主识别按钮必须能强制处理当前帧；同一页面不要再放置语义重复且易被宿主底栏遮挡的第二个“再次识别当前画面”。

加载/协议/运行时异常只由宿主兜底显示；子应用业务错误标记 `handled=true` 后宿主不得重复提示。
加载失败页至少显示 AppKey、PageKey、路由、版本、入口、HTTP 状态、发布状态、资产来源、挂载状态和安全原因码，并提供重试、返回与复制诊断。

### 首次挂载、iframe 交互与接口契约

- `/micro-app/:appKey/:microPath(.*)*` 是平台协议路由，不是租户菜单。它必须在首轮 URL 解析前作为宿主固定路由注册，且宿主组件要进入首屏主包；不能等登录后的菜单请求或异步路由注入，否则直接打开、书签或浏览器刷新会先命中空 `RouterView`，网络抖动时就表现为“刷新一次才出现”。页面是否存在仍由 `MicroApp/Resolve(RequirePage=true)` 和登录守卫校验，固定注册路由不授予菜单或数据权限。
- `micro-app` 的 `mounted` 生命周期只表示容器和脚本执行流程完成，不能单独证明子框架已经在 `#app` 渲染出可见内容。宿主给每次解析和重挂载分别下发 `hostGeneration`、`hostMountAttempt`；子应用在 Vue/React 根节点挂载并完成 `nextTick + requestAnimationFrame` 后用 `forceDispatch` 回传 `micro-app:ready`。宿主只接受与当前解析世代、挂载尝试都一致的就绪事件，再复核 `micro-app-body`、`#app` 的非空内容、可见样式以及非零宽高；`micro-app-body` 尚未创建必须判为“仍未渲染”，绝不能当成未知但成功。超时只自动销毁重建一次，第二次失败显示稳定诊断，禁止让白屏永久停留在 `mounted` 状态。
- iframe 内的 pointer/click 不会冒泡到宿主 document。子应用应在捕获阶段发送 `micro-app:interaction`，宿主再广播统一的“关闭全局浮层”事件。`@micro-zoe/micro-app` EventCenter 会去重内容相同的普通 dispatch，连续交互事件必须使用 `forceDispatch`；否则只能收到第一次点击。宿主的 Select/Popover 等浮层若 teleport 到 `body`，收到通知后还要显式执行组件的 click-outside/close 路径并清理搜索值和焦点，不能只依赖 `blur`。
- 调接口时以真实 `DosResult.Data` DTO 为准，不能仅凭界面名称猜成数组。例如在线终端接口返回的是用户视图对象 `{ Terminals: [...] }`，前端应读取 `Data.Terminals`，只把 `Data` 直接数组作为旧版兼容；列表 key 优先使用 `ConnectionId`、`DeviceClientId` 等稳定标识。修复前先沿 Controller → Service → 序列化对象 → 子应用解析完整核对契约，避免为正确的后端响应另写一个平行接口。
- 发布前统一更新 `package.json`、`package-lock.json` 根包版本、`.microi-micro-app.json`、应用 `CurrentVersion/BuildVersion`、微服务与页面 `BuildVersion`。应用商城候选包可以先嵌入待发布源码和构建产物，但 `.resource-sync-base` 只能在官方远端发布完成、逐文件内容哈希回读一致后由资源同步器推进，禁止把本地候选提前写成共同基线。

## 验收

- 源码、构建文件、运行时、页面路由和菜单五层分别回读。
- 组合发布成功前，私有源码回读必须与本地源码在路径集合、文件数、字节数、逐文件 SHA-256 和规范化清单哈希上完全一致；任何缺失、多余或读取错误都要阻止运行版本切换。
- 直接刷新友好路由与多个菜单至少往返 8 轮不 404、白屏、永久骨架屏、串页或实例名冲突；缓存范围内输入/筛选/滚动/内部路由保持。
- 打开第 6 个菜单实例后确认 LRU 只淘汰最旧隐藏实例且重入可冷启动；关闭 Tab、关闭其它/全部与退出登录后确认对应运行时已销毁。
- Dialog/Drawer 成功、取消、错误和关闭协议正确。
- 表单 `DevComponentPath` 能匹配页面 `LegacyComponentPaths`，指定路由正常加载；Add/Edit/View/只读、字段值回写和自动高度均通过。
- 独立地址覆盖“已有 Token 自动进入”和“无 Token 显示帐号密码”；`EnableCaptcha` 开/关各验一次，验证码响应头和登录参数正确。
- 宿主 API 请求携带当前 Token/OsClient/`permissionContext`，普通用户用真实授权模块成功、未授权模块仍明确拒绝。
- 至少验证桌面和窄屏；上传、表格、滚动、弹窗底部操作不被截断。
- 长弹窗滚动到顶部/中部/底部后，错误提示和确认层仍位于当前视口中央；自动化监听到原生 JavaScript 对话框直接判失败。
- 本地构建、MCP 发布和真实浏览器验收分别说明，未执行的层不宣称通过。
