@axewo/fin-micro 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +101 -3
  2. package/dist/browser.cjs +1 -1
  3. package/dist/browser.mjs +68 -66
  4. package/dist/core.cjs +1 -1
  5. package/dist/core.mjs +447 -433
  6. package/dist/index.cjs +1 -1
  7. package/dist/index.mjs +31 -24
  8. package/dist/{router-view-impl-C1KG6mxu.mjs → router-view-impl-B1NzCGKV.mjs} +18 -18
  9. package/dist/router-view-impl-BvOToV_Z.cjs +1 -0
  10. package/dist/runtime.cjs +1 -1
  11. package/dist/runtime.mjs +40 -39
  12. package/dist/sdk.cjs +1 -1
  13. package/dist/sdk.mjs +92 -56
  14. package/dist/types/browser/index.d.ts +4 -0
  15. package/dist/types/core/index.d.ts +1 -1
  16. package/dist/types/index.d.ts +2 -0
  17. package/dist/types/runtime/lifecycle.d.ts +1 -1
  18. package/dist/types/sdk/index.d.ts +63 -4
  19. package/dist/types/vue/index.d.ts +13 -2
  20. package/dist/types/vue/router-view-impl.d.ts +1 -1
  21. package/dist/vue.cjs +1 -1
  22. package/dist/vue.mjs +49 -35
  23. package/package.json +2 -2
  24. package/scaffold/base/package.json.template +5 -1
  25. package/scaffold/base/scripts/check-runtime.mjs +43 -0
  26. package/scaffold/base/src/locales/en.ts +12 -0
  27. package/scaffold/base/src/locales/index.ts +7 -1
  28. package/scaffold/base/src/locales/zh-CN.ts +12 -0
  29. package/scaffold/base/src/main.ts +13 -5
  30. package/scaffold/base/src/router/demo.ts +6 -0
  31. package/scaffold/base/src/router/index.ts +13 -0
  32. package/scaffold/base/src/stores/demo.ts +12 -0
  33. package/scaffold/base/src/stores/index.ts +11 -0
  34. package/scaffold/base/src/utils/appConfig.ts +49 -0
  35. package/scaffold/base/src/utils/errorUtils.ts +92 -0
  36. package/scaffold/base/src/utils/requestUtils.ts +32 -11
  37. package/scaffold/base/src/views/DemoView.vue +26 -0
  38. package/scaffold/host/.env.example +11 -3
  39. package/scaffold/host/src/App.vue +1 -13
  40. package/scaffold/host/src/bootstrap.ts +40 -8
  41. package/scaffold/host/src/hooks/usePlatformSession.ts +40 -8
  42. package/scaffold/host/src/layout/HostHeader.vue +65 -5
  43. package/scaffold/host/src/layout/HostLayout.vue +65 -21
  44. package/scaffold/host/src/layout/HostMenu.vue +1 -0
  45. package/scaffold/host/src/layout/HostTabs.vue +41 -8
  46. package/scaffold/host/src/layout/HostWorkspace.vue +19 -7
  47. package/scaffold/host/src/localPages.ts +45 -0
  48. package/scaffold/host/src/platform.ts +41 -10
  49. package/scaffold/host/src/router/guards.ts +128 -0
  50. package/scaffold/host/src/router/index.ts +25 -0
  51. package/scaffold/host/src/router/targets.ts +30 -0
  52. package/scaffold/host/src/router/workspace.ts +63 -0
  53. package/scaffold/host/src/styles/shell.css +761 -0
  54. package/scaffold/host/src/utils/request.ts +30 -3
  55. package/scaffold/host/src/views/HomeView.vue +19 -0
  56. package/scaffold/host/src/views/LoginRoute.vue +16 -0
  57. package/scaffold/host/src/views/LoginView.vue +59 -9
  58. package/scaffold/host/src/views/NotFoundView.vue +1 -0
  59. package/scaffold/host/src/views/ProfileView.vue +24 -0
  60. package/scaffold/host/src/views/WorkspaceRoute.vue +11 -0
  61. package/scaffold/vue/src/bootstrap.ts +18 -22
  62. package/scaffold/vue/src/bridge/auth.ts +23 -0
  63. package/scaffold/vue/src/bridge/session.ts +2 -2
  64. package/scaffold/vue/src/main.ts +5 -2
  65. package/scaffold/vue/src/router/index.ts +2 -0
  66. package/scaffold/vue/src/views/DetailView.vue +14 -5
  67. package/scaffold/vue/src/views/ListView.vue +9 -5
  68. package/dist/router-view-impl-CgLEalx4.cjs +0 -1
package/README.md CHANGED
@@ -46,7 +46,7 @@ const dispose = setupWujieLifecycle({ mount, unmount })
46
46
  if (import.meta.hot) import.meta.hot.dispose(dispose)
47
47
  ```
48
48
 
49
- `setupWujieLifecycle` 统一注册 Wujie 回调、通知嵌入挂载、独立启动、unload 和幂等清理;重复注册会释放旧注册。`dispose()` 返回 Promise,可等待清理完成。错误会报告至 console.error,显式调用生命周期回调可接收失败。bootstrap 自身仍须在 unmount 时使异步初始化失效,封装不能撤销回调内部副作用。SSR 检测为 false,注册不启动应用。环境检测不会把普通 iframe 或自行注册的挂载函数误认为微前端,不探测其他未适配的微前端框架。
49
+ `setupWujieLifecycle` 统一注册 Wujie 回调、通知嵌入挂载、独立启动、pagehide 和幂等清理;重复注册会释放旧注册。`pagehide.persisted === true` 时保留实例与监听,避免 BFCache 返回后留下已销毁的应用;普通离开才释放注册和应用。Wujie 显式 unmount、dispose 与 HMR 清理不受 persisted 影响,不注册 unload,也不通过放宽 Permissions-Policy 掩盖警告。`dispose()` 返回 Promise,可等待清理完成。错误会报告至 console.error,显式调用生命周期回调可接收失败。bootstrap 自身仍须在 unmount 时使异步初始化失效,封装不能撤销回调内部副作用。SSR 检测为 false,注册不启动应用。环境检测不会把普通 iframe 或自行注册的挂载函数误认为微前端,不探测其他未适配的微前端框架。
50
50
 
51
51
  基座使用普通启动入口,只有 vue 子应用模板接入上述封装;不再在 base 中注册子应用专属全局回调。
52
52
 
@@ -72,16 +72,114 @@ node packages/fin-micro/cli/index.mjs create ./order-app --template vue --app-id
72
72
 
73
73
  `host` 默认端口 7100,`vue` 默认端口 7101;项目名与 appId 只接受小写字母、数字和连字符。子应用示例建议指定 `--app-id business`,与基座默认登记保持一致;或调整基座的环境变量。目标目录必须不存在,父目录必须已存在;命令不覆盖文件、不自动安装依赖、不自动注册远端应用、不执行发布。进入生成项目后运行 `pnpm install`、`pnpm dev` 和 `pnpm build`。生成源码属于项目,CLI 不升级或覆盖既有源码。
74
74
 
75
- 基座联调时,在 `.env.local` 显式配置 `VITE_AUTH_DEMO=true` 才能启用开发演示身份;正式模式请求 `/api/session`、`/api/login`、`/api/menus`、`/api/logout`,协议与替换点见快速开始章节。后台菜单只提供授权路由,应用执行地址来自可信本地注册表。
75
+ ### host 本地登录与主页体验
76
+
77
+ 用户未指定已生成项目时,修改目标是 `scaffold/host`,不会自动覆盖其他生成项目。仓库内可在一个已存在的父目录创建全新项目:
78
+
79
+ ```sh
80
+ node packages/fin-micro/cli/index.mjs create ./my-host --template host
81
+ # 进入 my-host 后安装依赖并启动(需要可用的 fin-core / fin-ui / fin-micro 对应版本)
82
+ pnpm install
83
+ pnpm dev
84
+ ```
85
+
86
+ 打开 Vite 输出的本机地址(默认端口 7100),点击“演示登录,进入主页”。开发默认启用演示,不需要后端或子应用,不展示或收集账号密码,固定显示“演示用户”。登录后默认打开真正的本地主页;左侧 Element 菜单、顶部搜索、个人菜单均可打开“主页 / 个人中心”。个人页展示公开身份,可回主页或退出。主页与 `apps/micro-host` 一样属于唯一 core tabs,而非独立入口或第二套标签状态:认证后在恢复前同步登记,初始、直链和刷新恢复均固定首位且不重复;缺少主页或主页顺序错误的旧快照自动迁移,业务页之间的顺序不变。主页无关闭按钮,右键菜单禁用刷新与关闭;“关闭全部业务页 / 其他 / 左侧 / 右侧”始终排除主页(个人中心也属于可关闭页)。业务页仍可正常刷新;关闭活动页在一次核心事务内回落主页,URL 经同一 Vue Router 更新,守卫拒绝则保持原页和 URL。不放置虚假业务数据或未实现业务按钮。
87
+
88
+ 刷新仅凭当前标签页 `sessionStorage` 的 `fin-micro:demo-session:v1=1` 标记恢复固定演示身份,随后通过原 `connectMicroBrowser` 恢复已授权页签,产生新的生命周期 key。它不是 Token,不含账号密码,不访问认证接口。禁用浏览器存储时仍可登录,但无法跨刷新恢复。退出须经原核心离页守卫;确认后关闭全部页签、清除演示标记与私有凭据。认证失效也清除标记,组件卸载不清除标记以支持刷新。
89
+
90
+ 如需真实后端联调,在 `.env.local` 设置 `VITE_AUTH_DEMO=false`。生产构建始终禁止演示,即使设置 true;请求 `/api/session`、`/api/login`、`/api/menus`、`/api/logout`,任何网络、认证或菜单失败均不降级演示。部署前必须接入真实认证、服务端授权及 CSRF 防护;开发标记不具有任何生产认证效力。后台菜单只提供授权路由,应用执行地址来自可信本地注册表。
91
+
92
+ 本地页使用明确的 `{ kind: 'local', appId: 'host-local' }` 注册,禁止 entry;`host-local` 为宿主保留 appId,不能配置给远端应用。`localPages.ts` 固定 route/tabId 白名单,`HostWorkspace` 直接使用 `HomeView/ProfileView` SFC,Wujie 明确拒绝 local;不会把本地页面伪装成 HTTP 远端。两类页面复用同一 core 的标识、激活、关闭、刷新、守卫和 browser 恢复。远端仍按 appId 单实例挂载,Token 与 Auth 失败能力只向原可信业务应用白名单授权。可选业务入口需要启动子应用,但它离线不影响本地工作台。
76
93
 
77
94
  `scaffold/base` 保存公共骨架,`scaffold/host`、`scaffold/vue` 按类型覆盖差异文件;两类模板统一接入 fin-ui。基座仅包含登录、公开用户信息、菜单与微前端平台编排,不包含系统管理业务。认证示例不是生产认证,需替换接口适配并补齐服务端鉴权;应用入口、appId、菜单权限需按实际配置。
78
95
 
79
96
  运行库通过 npm 依赖导入,不复制 core 源码。公开包使用 files 白名单,仅交付运行库 JS、类型声明、创建命令与可公开的骨架源码,不交付运行库 src 或 sourcemap。编译 JavaScript 仍然可被分析,不应含密钥或保密逻辑。库升级需回归、构建、部署,无需重新创建项目;模板更新不会自动迁移生成项目。
80
97
 
81
- 生成项目的 `src` 统一具有 `stores/router/layout/locales/hooks/utils/types/components/views/styles`。布局放 layout,页面放 views,公共样式放 styles;未启用的状态管理仅保留目录,不新增 plugins 目录。公共 `components/AppProviders.vue` 使用 `FConfigProvider` 与 `ElConfigProvider` 包裹 host/vue 全树,以 `useFinLocale()` 的全局 locale 响应式选择 fin-ui 的 `zhCN/en` 和 Element Plus 语言包,并同步 size(默认 default)。`locales/index.ts` 通过 fin-ui 重导出的 `installFinI18n` 在每次 createApp 后、mount 前独立安装,默认 zh-CN;不共享模块级 i18n 实例,`config.locale` 仅是语言包数据,不负责安装插件。模板显式安装 fin-locale 要求的 peer 依赖 `vue-i18n ^9.14.0`;业务子组件可用 `useFinLocale().locale.value = 'en'` 切换当前应用语言,不自动跨应用广播或翻译硬编码业务文案。公共 `base/src/utils/requestUtils.ts` 同时生成到基座与 Vue 子应用,通过 `@axewo/fin-core/http/axios` 为各项目创建独立 `AxiosHttpClient`,提供请求/响应拦截器的应用级替换点。基座平台适配使用专属 `utils/request.ts` 的独立 Axios 实例,配置原生 timeout/withCredentials;平台显式处理 Axios 401,并以局部 Token 请求菜单/注销。子应用不生成该平台实例。业务中使用 `import { http } from '../utils/requestUtils'`(未配置别名时使用相对路径),调用 `http.get/post`;返回整个响应体,业务 code/data 不自动解包。子应用拦截器通过 `http.interceptors` 配置,响应拦截器需保留 AxiosResponse,Token/401策略由业务明确接入。依赖含该Axios入口的 fin-core 2.1.0 源码版本,公开安装前需先发布该依赖;此前旧2.1.0候选tgz不包含Axios入口,不能用于本次模板。本地联合安装前需重新构建、打包 core 与 micro,旧候选包不能代表本次源码。
98
+ 生成项目的 `src` 统一具有 `stores/router/layout/locales/hooks/utils/types/components/views/styles`。布局放 layout,页面放 views,公共样式放 styles,不新增 plugins 目录。公共依赖显式包含 Vue Router 4.5.0 与 Pinia 2.3.1(支持模板的 Vue 3.5.13 / TypeScript 5.6.3)。`base/src/router/index.ts` 保留通用 memory 工厂,host overlay 的 `router/index.ts` 则提供 `createAppRouter(controller, history?)`,默认使用 `createWebHistory(BASE_URL)`,由 Vue Router 独占顶级路径。`App.vue` 仅包含 Provider 与 `RouterView`,不再以 v-if 切换登录和工作区。vue overlay 继续使用 `createMicroRouter(embedded)`,嵌入模式 memory、独立模式 hash,保留列表/详情路由。公共 `stores/index.ts` 的 `createAppPinia()` 每次返回新容器;两个 bootstrap 均在 mount 前安装 router/Pinia,在卸载、失败和重挂载时销毁本轮 history 及 Pinia 作用域,不共享应用级状态。公共 `components/AppProviders.vue` 使用 `FConfigProvider` 与 `ElConfigProvider` 包裹 host/vue 全树,以 `useFinLocale()` 的全局 locale 响应式选择 fin-ui 的 `zhCN/en` 和 Element Plus 语言包,并同步 size(默认 default)。`locales/index.ts` 通过 fin-ui 重导出的 `installFinI18n` 在每次 createApp 后、mount 前独立安装,默认 zh-CN;不共享模块级 i18n 实例,`config.locale` 仅是语言包数据,不负责安装插件。模板显式安装 fin-locale 要求的 peer 依赖 `vue-i18n ^9.14.0`;业务子组件可用 `useFinLocale().locale.value = 'en'` 切换当前应用语言,不自动跨应用广播或翻译硬编码业务文案。公共 `base/src/utils/requestUtils.ts` 同时生成到基座与 Vue 子应用,通过 `@axewo/fin-core/http/axios` 为各项目创建独立 `AxiosHttpClient`,提供请求/响应拦截器的应用级替换点。基座平台适配使用专属 `utils/request.ts` 的独立 Axios 实例,配置原生 timeout/withCredentials;平台显式处理 Axios 401,并以局部 Token 请求菜单/注销。子应用不生成该平台实例。业务中使用 `import { http } from '../utils/requestUtils'`(未配置别名时使用相对路径),调用 `http.get/post`;返回整个响应体,业务 code/data 不自动解包。子应用拦截器通过 `http.interceptors` 配置,响应拦截器需保留 AxiosResponse,Token/401策略由业务明确接入。模板当前声明 fin-core 2.1.1,必须实际具备该 Axios 子路径和编译导出;其他版本同样以实际导出及兼容回归为准。本地联合安装前需重新构建、打包 core 与 micro,旧候选包不能代表本次源码。
82
99
 
83
100
  地址栏修改仍经过基座 canOpen;子应用本地路由不是权限表。独立子应用当前为演示,直接访问路由可以显示示例页;生产独立运行需接身份/权限检查或禁用独立业务模式。前端检查不能代替后台 API 的登录、操作权限与资源归属验证;隐藏页面可有显式授权,不能只以菜单是否展示判断访问权限。
84
101
 
102
+ ### host 真实路由与 URL 所有权
103
+
104
+ - `/login`:真实登录路由;登录成功只允许跳转到当前会话已授权的内部目标,外链、协议相对地址、反斜杠、登录循环、未知或未授权目标均回落 `/home`。
105
+ - `/home`、`/profile`:已认证工作区中的本地页;`/workspace?tab=...` 是远端活动页的公开导航快照(appId/tabId/route,不含凭据)。`/` 重定向主页,未匹配路径展示 404。
106
+ - `bootstrap` 每个 App 仅创建一个 `createPlatformSession()`,一次恢复 Promise 供初始路由守卫等待;`usePlatformSession()` 仅注入,不重复 restore 或注册 401 监听。正常退出先经 core 关闭全部页签的守卫再撤销会话并进入 `/login`;401 失效立即撤销,不被离页确认阻断。
107
+ - host 的 `connectWorkspaceBrowser` 先登记固定主页,再使用 `connectMicroBrowser(host, { url: false, initialLocation, storageKey })` 仅恢复/持久化用户隔离页签。`router/workspace.ts` 统一协调 Vue Router 和 core:UI 导航写入 router,直链/返回经过路由与页签守卫。连接器不再监听或覆盖 host 的 hash;未配置 `url:false` 的旧使用方式仍保留 hash 协议。
108
+ - 首次深链优先于存储,恢复逐项经过 `canOpen`;整页刷新产生新的 core key,不恢复 Token、草稿或沙箱实例。生产服务器必须将 `/login`、`/home`、`/profile`、`/workspace` 及其他 SPA 路径回退到该应用 `index.html`,静态资源与 `/api/` 不应被改写。子目录部署需同时配置 Vite base 和服务器回退目录。
109
+
110
+ 交互关闭统一通过宿主 `workspaceOperation` 加上 `exclude` 与 `closeFallback`,不修改通用库的默认关闭规则。`close/closeMany.closeFallback` 是可选的生命周期引用:只在活动页被关闭且目标仍存活时同事务回落,不创建页面;旧 key 不复活重开页,页面 SDK 不能借它跨应用导航。正常退出直接提交不排除主页的完整 `closeMany all`,仍尊重业务守卫;真实失效或 host dispose 直接清理全部,不被固定页策略阻断。固定不是权限或资源保留机制;底层宿主原语仍允许显式清理。模板与运行库需使用本次配套构建,不能仅更新模板沿用旧 dist。
111
+
112
+ ### host 统一路由守卫与扩展
113
+
114
+ `router/guards.ts` 是唯一 `beforeEach` 注册入口,执行顺序为:等待 `controller.ready` → 读取最新身份 → 登录与安全 redirect → 固定路由/远端授权和可选 `meta.permissions` → `beforeEnter` 业务扩展 → 当前 workspace 核心事务。`router/index.ts` 只声明路由并安装守卫,`router/workspace.ts` 使用 `attachWorkspaceGuard` 挂接现有 core `operate/beforeLeave`,不再注册第二个前置守卫。未登录受保护页进入 `/login?redirect=...`;已登录访问 `/login` 只返回经过 `authorizedRedirect` 检查的内部授权目标。404 保持独立兜底;已登录工作区离开到 404 仍须通过业务未保存确认。
115
+
116
+ 公开结构:
117
+
118
+ - `installHostGuards(router, controller, options?) → dispose`:每个 router 只允许一个安装;返回幂等撤销函数。
119
+ - `HostGuardOptions.beforeEnter({ to, from, session, signal }) → boolean | void | Promise<boolean | void>`:在内置授权后执行,返回 false 取消,true/void 继续;抛错或拒绝会取消并报告,不能凭扩展放宽内置白名单。
120
+ - `HostGuardOptions.onError(error)`:供宿主脱敏展示,不直接把原始异常写入 UI。
121
+ - `attachWorkspaceGuard(router, delegate) → detach`:只供工作区桥接核心离页事务,重复连接拒绝;卸载撤销旧委托,不能通过组件另建 beforeEach。
122
+ - `createAppRouter(controller, history?, options?) → router & { disposeGuards() }`:bootstrap 在卸载、启动失败和重挂载时显式撤销守卫,再销毁 history。
123
+
124
+ 可在 bootstrap 的原 `createAppRouter` 调用处增加业务限制,**不要另调 router.beforeEach**:
125
+
126
+ ```ts
127
+ const router = createAppRouter(controller, undefined, {
128
+ onError: reportError,
129
+ beforeEnter: ({ to, session, signal }) => {
130
+ if (signal.aborted) return false
131
+ // 示例:业务条件只能进一步限制已获本地路由白名单授权的入口。
132
+ return to.path !== '/profile' || Boolean(session.user.id)
133
+ },
134
+ })
135
+ ```
136
+
137
+ 路由记录可加 `meta: { authenticated: true, permissions: ['demo:read'] }`,表示必须具备全部声明权限;生产替换为真实权限码,同时补齐本地 route/tabId 白名单与页面映射,不能仅靠 meta 添加任意受保护页面。该前端检查不代替后台授权。
138
+
139
+ 新导航、身份替换或撤销会中止旧守卫等待;每次异步边界后复查导航代次和身份对象,迟到恢复/扩展结果不能放行旧导航。取消信号只撤销导航等待,不自动取消已经发生的保存或外部副作用。正常退出仍先完成核心离页事务;真实失效及注销撤销身份后跳过旧 workspace 委托,优先清理资源。首次恢复提交身份属于 ready 本身,不会误取消首次直链或刷新导航。
140
+
141
+ ### 启动依赖门禁
142
+
143
+ 生成项目在 `npm run dev/build` 前自动执行 `scripts/check-runtime.mjs`,也可单独运行 `npm run check:runtime`。它加载实际安装包并校验模板使用的导出,host 额外要求 `@axewo/fin-micro` 导出 `hostRouterProtocol === 1`(local、认证能力、仅存储连接器),缺失时在 Vite 链接前明确报告模块/API。版本号由生成器实际 package.json 注入;同版本的旧构建仍可能缺能力,不能靠版本字符串保证可用。请安装配套构建的 core/UI/micro 包,模板不会自动升级或降级依赖,也不能以源码 alias 验证代替实际安装验证。直接运行 `vite` 会绕过 npm 的前置门禁,不推荐。
144
+
145
+ DEV 的启动界面只增加固定阶段 `load-bootstrap/mount` 和白名单标准错误名;模块 `SyntaxError` 会提示先运行能力检查。生产仍只显示固定脱敏文案;两种环境均不回显异常 message、Token、请求/响应或堆栈。配置类错误需核对 `.env` 的公开配置;能力门禁不是后端连通性或接口协议测试。
146
+
147
+ ### 公共配置、错误边界与登录失效
148
+
149
+ 公共 `utils/appConfig.ts` 集中校验 `VITE_API_BASE`(默认 `/api/`)、`VITE_TRUSTED_API_BASE`(默认与 API 一致)、`VITE_API_TIMEOUT`(默认 30000 毫秒)及 `VITE_API_CREDENTIALS`(仅接受字符串 true/false,默认 false)。两个 API 目录必须完全一致、以 `/` 结尾;拒绝 userinfo、查询、片段、路径穿越及编码分隔符。跨源生产网关与应用代码入口使用 HTTPS,本机开发允许 HTTP。环境变量是公开部署配置,不能包含秘密。跨源 Cookie 需后端精确 CORS 与 CSRF 防护;默认相对 API 属于当前子应用运行源,并非自动代理到宿主。
150
+
151
+ `utils/errorUtils.ts` 将 Axios/HTTP/SDK 错误统一映射为取消、未登录、无权限、超时、网络及未知错误。界面与日志仅消费固定安全文案,不展示原始 message、响应体、URL、头、Token 或堆栈;取消静默。Vue `errorHandler`、登录表单、导航与连接失败使用相同分类。入口动态加载 bootstrap,因此模块加载/配置校验错误也有纯文本启动兜底与重试;卸载使晚到导入失效,失败与重挂载继续清理 router/Pinia/凭据订阅。构建配置错误需要更正环境并重新构建/刷新;重试不保证浏览器重新求值已失败的 ESM 模块。
152
+
153
+ Vue 嵌入时协商并安装 `getToken/beginAuthRequest/reportAuthFailure`。`createAuthFailureCapabilities` 是 SDK 正式契约,复用 `callHost` 白名单和生命周期;每次可信请求先获取不含凭据的不透明内存票据,绑定宿主私有凭据版本与可信 appId。401 或 Token 未登录按原票据报告,宿主核验后立即撤销身份、推进版本;并发仅失效一次,旧响应不能退出新登录。票据不得复制、序列化、持久化或用于 iframe RPC。平台自身请求也在发送时捕获私有版本。正常主动退出仍等待页面守卫,真实认证失效不允许被未保存守卫阻止。模板不刷新 Token、不自动重放任何请求,也不新增总线或请求库。
154
+
155
+ 独立 Vue 演示不安装 bridge 身份、不生成已登录快照,权限默认全部拒绝;列表/详情仅显式允许本地内存展示,不表示获得平台或 API 权限。生产独立部署应安装自己的认证与权限准入,或禁用该演示路径。基座演示身份另由 `DEV && VITE_AUTH_DEMO !== 'false'` 控制(开发默认启用),真实接口失败绝不降级为演示登录。
156
+
157
+ ### 响应式权限示例
158
+
159
+ ```ts
160
+ import { computed } from 'vue'
161
+ import { usePermission } from '@axewo/fin-micro/vue'
162
+
163
+ // identity 来自 createBridgeSession / subscribeSession,不读取或存储 Token。
164
+ const permission = usePermission(identity)
165
+ const canRead = permission.can('demo:read')
166
+ const canEdit = computed(() => permission.hasPermission('demo:write'))
167
+ const canUse = computed(() => permission.hasAny(['demo:read', 'demo:write']))
168
+ const canManage = computed(() => permission.hasAll(['demo:read', 'demo:write']))
169
+ ```
170
+
171
+ 框架无关逻辑可直接从 `/sdk` 导入 `hasPermission(session, name)`、`hasAny(session, names)`、`hasAll(session, names)`。未登录、缺失会话、空权限名及空数组均安全拒绝;不支持通配符或角色推断。Vue 权限变更/退出立即更新计算值;普通函数在 computed/模板内调用才追踪依赖,不要把单次布尔结果当作响应式状态。按钮显隐仅辅助体验,核心 canOpen 和服务端仍须独立鉴权。
172
+
173
+ ### locales / router / stores 联动示例
174
+
175
+ 两类生成项目均包含 `locales/zh-CN.ts`、`locales/en.ts`、`stores/demo.ts`、`router/demo.ts` 和 `views/DemoView.vue`。`installAppLocale` 将 `demo.*` 业务词条合并到 fin-ui 内置语言包;页面通过全局 `useI18n` 切换中文/英文,通过 `storeToRefs(useDemoStore())` 展示计数与双倍值,并调用 `increment/reset`。状态只属于本轮应用的 Pinia,不持久化、不跨应用共享,也不存储 Token。
176
+
177
+ - **vue 独立运行**:启动生成项目后访问 `/#/demo`(默认端口 7101)。`createMicroRouter` 复用 `demoRoutes`,原列表/详情与缓存接线不变。
178
+ - **vue 嵌入基座**:仅注册本地 `/demo` 不会获得访问授权。后台 `/api/menus` 须为当前已授权应用返回菜单,例如 `{ menuId: 'demo', name: '基础示例', target: { tabId: 'business:demo', route: '/demo' } }`(替换实际 appId 前缀),再从基座菜单打开。模板不自动添加 demo 菜单,也不放宽 `canOpenRoute`;未授权深链仍被拒绝。
179
+ - **host 本地扩展**:在 host 的 `router/index.ts` 添加真实路由记录及认证元信息,并在 `router/targets.ts` 补充固定路由到 core 页签的映射;工作区页面还需补充 `localPages.ts` 的精确白名单及 `HostWorkspace.vue` 的显式 SFC 映射。不可直接把 `demoRoutes` 传给会话参数,亦不可用本地路由代替平台授权。
180
+
181
+ 组件外使用 store 时显式传所属实例:`const demo = useDemoStore(pinia)`;组件内由已安装的 Pinia 注入。多页签属于同一子应用时共享该应用的计数,这是应用级状态,不是页签级缓存。
182
+
85
183
  ## 包入口
86
184
 
87
185
  | 入口 | 用途 |
package/dist/browser.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function H(o){return`#/apps/${encodeURIComponent(o.appId)}/${encodeURIComponent(o.tabId)}?route=${encodeURIComponent(o.route)}`}function U(o){if(!o||o==="#/"||o==="#")return null;const p=/^#\/apps\/([^/?#]+)\/([^/?#]+)\?route=([^&#]*)$/.exec(o);if(!p)throw new Error("无效的微应用 URL");const[,r,v,h]=p;return{appId:decodeURIComponent(r),tabId:decodeURIComponent(v),route:decodeURIComponent(h)}}function P(o,p={}){const r=p.window??window,v=p.storageKey??"fin-micro:tabs:v1",h="fin-micro:navigation:v1",L=`${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`,f=t=>{var e;try{(e=p.onError)==null||e.call(p,t instanceof Error?t:new Error(String(t)))}catch{}},A=new Set(o.apps.map(t=>t.appId)),w=(t,e)=>typeof t=="string"&&t.length>0&&t.length<=e&&!/[\u0000-\u001f\u007f]/.test(t);function m(t){if(!t||typeof t!="object")return!1;const e=t;return w(e.appId,200)&&A.has(e.appId)&&w(e.tabId,500)&&w(e.title,500)&&w(e.route,8192)&&e.route.startsWith("/")&&!e.route.startsWith("//")&&!e.route.includes("\\")}const M=t=>({appId:t.appId,tabId:t.tabId,title:t.title,route:t.route,...t.activeMenuId!==void 0?{activeMenuId:t.activeMenuId}:{}});function $(t){try{r.sessionStorage.setItem(v,JSON.stringify({version:1,tabs:t.tabs.map(M),activeTabId:t.activeTabId}))}catch(e){f(e)}}function k(t,e=!1){try{const n=t.tabs.find(R=>R.tabId===t.activeTabId),s=n?H(n):"#/",a=r.history.state,c=a==null?void 0:a[h],d=r.location.hash===s;if(d&&(c==null?void 0:c.session)===L&&(c==null?void 0:c.key)===((n==null?void 0:n.key)??null))return;const i=`${r.location.pathname}${r.location.search}${s}`,b={...a&&typeof a=="object"?a:{},[h]:{session:L,key:(n==null?void 0:n.key)??null}};e&&!d?r.history.pushState(b,"",i):r.history.replaceState(b,"",i)}catch(n){f(n)}}function K(t){const e=o.getState().tabs.find(s=>s.tabId===t.tabId),n={...t,title:(e==null?void 0:e.title)??`${t.appId} · ${t.route}`.slice(0,500)};if(!m(n))throw new Error("URL 包含未知应用或无效页签/路由");o.openTab(n)}let u=null;try{u=U(r.location.hash)}catch(t){f(t)}try{const t=r.sessionStorage.getItem(v);if(t){const e=JSON.parse(t);if(!e||typeof e!="object")throw new Error("无效的页签存储");const n=e;if(n.version!==1||!Array.isArray(n.tabs)||n.tabs.length>100)throw new Error("不支持的页签存储版本或格式");const s=new Set(o.getState().tabs.map(a=>a.tabId));for(const a of n.tabs)if(!(!m(a)||s.has(a.tabId))&&!(u&&m({...u,title:"深链恢复"})&&u.tabId===a.tabId&&(u.appId!==a.appId||u.route!==a.route)))try{o.openTab(M(a)),s.add(a.tabId)}catch(c){f(c)}typeof n.activeTabId=="string"&&o.activateTab(n.activeTabId)}}catch(t){f(t)}try{u&&K(u)}catch(t){f(t)}let l=o.getState();k(l),$(l);let E=!1,y=!1;const O=o.subscribe(t=>{const e=l.tabs.some(n=>!t.tabs.some(s=>s.key===n.key));l=t,$(t),E||k(t,!e)});let g=0,I,S;function T(t){var c;if(y)return;const e=++g;I==null||I.abort();const n=new AbortController;I=n;const s=r.location.hash,a=(c=r.history.state)==null?void 0:c[h];E=!0,(async()=>{await Promise.resolve();try{if(y||e!==g)return;const d=U(s);if(!d)return;const i=o.getState().tabs.find(R=>R.tabId===d.tabId),b={...d,title:(i==null?void 0:i.title)??`${d.appId} · ${d.route}`.slice(0,500)};if(!m(b))throw new Error("URL 包含未知应用或无效页签/路由");if(t){if((a==null?void 0:a.session)!==L||!i||a.key!==i.key||i.appId!==b.appId||i.route!==b.route)return;await o.operate({type:"activate",target:{tabId:i.tabId,key:i.key},signal:n.signal})}else await o.operate({type:"open",input:b,signal:n.signal})}catch(d){!y&&e===g&&f(d)}finally{!y&&e===g&&(E=!1,l=o.getState(),k(l),$(l))}})()}const C=()=>{S=r.location.hash,T(!0)},j=t=>{const e=t.newURL,n=e?new URL(e).hash:r.location.hash;if(S===n){S=void 0;return}S=void 0,T(!1)};return r.addEventListener("popstate",C),r.addEventListener("hashchange",j),{dispose(){y||(y=!0,g++,I==null||I.abort(),O(),r.removeEventListener("popstate",C),r.removeEventListener("hashchange",j))}}}exports.connectMicroBrowser=P;exports.decodeMicroLocation=U;exports.encodeMicroLocation=H;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function H(o){return`#/apps/${encodeURIComponent(o.appId)}/${encodeURIComponent(o.tabId)}?route=${encodeURIComponent(o.route)}`}function U(o){if(!o||o==="#/"||o==="#")return null;const i=/^#\/apps\/([^/?#]+)\/([^/?#]+)\?route=([^&#]*)$/.exec(o);if(!i)throw new Error("无效的微应用 URL");const[,r,v,h]=i;return{appId:decodeURIComponent(r),tabId:decodeURIComponent(v),route:decodeURIComponent(h)}}function P(o,i={}){const r=i.window??window,v=i.storageKey??"fin-micro:tabs:v1",h="fin-micro:navigation:v1",S=`${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`,p=t=>{var e;try{(e=i.onError)==null||e.call(i,t instanceof Error?t:new Error(String(t)))}catch{}},A=new Set(o.apps.map(t=>t.appId)),w=(t,e)=>typeof t=="string"&&t.length>0&&t.length<=e&&!/[\u0000-\u001f\u007f]/.test(t);function m(t){if(!t||typeof t!="object")return!1;const e=t;return w(e.appId,200)&&A.has(e.appId)&&w(e.tabId,500)&&w(e.title,500)&&w(e.route,8192)&&e.route.startsWith("/")&&!e.route.startsWith("//")&&!e.route.includes("\\")}const M=t=>({appId:t.appId,tabId:t.tabId,title:t.title,route:t.route,...t.activeMenuId!==void 0?{activeMenuId:t.activeMenuId}:{}});function $(t){try{r.sessionStorage.setItem(v,JSON.stringify({version:1,tabs:t.tabs.map(M),activeTabId:t.activeTabId}))}catch(e){p(e)}}function k(t,e=!1){if(i.url!==!1)try{const n=t.tabs.find(R=>R.tabId===t.activeTabId),s=n?H(n):"#/",a=r.history.state,d=a==null?void 0:a[h],f=r.location.hash===s;if(f&&(d==null?void 0:d.session)===S&&(d==null?void 0:d.key)===((n==null?void 0:n.key)??null))return;const c=`${r.location.pathname}${r.location.search}${s}`,b={...a&&typeof a=="object"?a:{},[h]:{session:S,key:(n==null?void 0:n.key)??null}};e&&!f?r.history.pushState(b,"",c):r.history.replaceState(b,"",c)}catch(n){p(n)}}function K(t){var s;const e=o.getState().tabs.find(a=>a.tabId===t.tabId),n={...t,title:(e==null?void 0:e.title)??((s=i.initialLocation)==null?void 0:s.title)??`${t.appId} · ${t.route}`.slice(0,500)};if(!m(n))throw new Error("URL 包含未知应用或无效页签/路由");o.openTab(n)}let u=null;try{u=i.initialLocation??(i.url===!1?null:U(r.location.hash))}catch(t){p(t)}try{const t=r.sessionStorage.getItem(v);if(t){const e=JSON.parse(t);if(!e||typeof e!="object")throw new Error("无效的页签存储");const n=e;if(n.version!==1||!Array.isArray(n.tabs)||n.tabs.length>100)throw new Error("不支持的页签存储版本或格式");const s=new Set(o.getState().tabs.map(a=>a.tabId));for(const a of n.tabs)if(!(!m(a)||s.has(a.tabId))&&!(u&&m({...u,title:"深链恢复"})&&u.tabId===a.tabId&&(u.appId!==a.appId||u.route!==a.route)))try{o.openTab(M(a)),s.add(a.tabId)}catch(d){p(d)}typeof n.activeTabId=="string"&&o.activateTab(n.activeTabId)}}catch(t){p(t)}try{u&&K(u)}catch(t){p(t)}let l=o.getState();k(l),$(l);let E=!1,y=!1;const O=o.subscribe(t=>{const e=l.tabs.some(n=>!t.tabs.some(s=>s.key===n.key));l=t,$(t),E||k(t,!e)});let g=0,I,L;function T(t){var d;if(y)return;const e=++g;I==null||I.abort();const n=new AbortController;I=n;const s=r.location.hash,a=(d=r.history.state)==null?void 0:d[h];E=!0,(async()=>{await Promise.resolve();try{if(y||e!==g)return;const f=U(s);if(!f)return;const c=o.getState().tabs.find(R=>R.tabId===f.tabId),b={...f,title:(c==null?void 0:c.title)??`${f.appId} · ${f.route}`.slice(0,500)};if(!m(b))throw new Error("URL 包含未知应用或无效页签/路由");if(t){if((a==null?void 0:a.session)!==S||!c||a.key!==c.key||c.appId!==b.appId||c.route!==b.route)return;await o.operate({type:"activate",target:{tabId:c.tabId,key:c.key},signal:n.signal})}else await o.operate({type:"open",input:b,signal:n.signal})}catch(f){!y&&e===g&&p(f)}finally{!y&&e===g&&(E=!1,l=o.getState(),k(l),$(l))}})()}const C=()=>{L=r.location.hash,T(!0)},j=t=>{const e=t.newURL,n=e?new URL(e).hash:r.location.hash;if(L===n){L=void 0;return}L=void 0,T(!1)};return i.url!==!1&&(r.addEventListener("popstate",C),r.addEventListener("hashchange",j)),{dispose(){y||(y=!0,g++,I==null||I.abort(),O(),i.url!==!1&&(r.removeEventListener("popstate",C),r.removeEventListener("hashchange",j)))}}}exports.connectMicroBrowser=P;exports.decodeMicroLocation=U;exports.encodeMicroLocation=H;
package/dist/browser.mjs CHANGED
@@ -1,25 +1,25 @@
1
- function x(o) {
2
- return `#/apps/${encodeURIComponent(o.appId)}/${encodeURIComponent(o.tabId)}?route=${encodeURIComponent(o.route)}`;
1
+ function x(r) {
2
+ return `#/apps/${encodeURIComponent(r.appId)}/${encodeURIComponent(r.tabId)}?route=${encodeURIComponent(r.route)}`;
3
3
  }
4
- function H(o) {
5
- if (!o || o === "#/" || o === "#") return null;
6
- const p = /^#\/apps\/([^/?#]+)\/([^/?#]+)\?route=([^&#]*)$/.exec(o);
7
- if (!p) throw new Error("无效的微应用 URL");
8
- const [, r, v, y] = p;
4
+ function H(r) {
5
+ if (!r || r === "#/" || r === "#") return null;
6
+ const i = /^#\/apps\/([^/?#]+)\/([^/?#]+)\?route=([^&#]*)$/.exec(r);
7
+ if (!i) throw new Error("无效的微应用 URL");
8
+ const [, o, v, y] = i;
9
9
  return {
10
- appId: decodeURIComponent(r),
10
+ appId: decodeURIComponent(o),
11
11
  tabId: decodeURIComponent(v),
12
12
  route: decodeURIComponent(y)
13
13
  };
14
14
  }
15
- function J(o, p = {}) {
16
- const r = p.window ?? window, v = p.storageKey ?? "fin-micro:tabs:v1", y = "fin-micro:navigation:v1", L = `${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`, u = (t) => {
15
+ function J(r, i = {}) {
16
+ const o = i.window ?? window, v = i.storageKey ?? "fin-micro:tabs:v1", y = "fin-micro:navigation:v1", S = `${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`, u = (t) => {
17
17
  var e;
18
18
  try {
19
- (e = p.onError) == null || e.call(p, t instanceof Error ? t : new Error(String(t)));
19
+ (e = i.onError) == null || e.call(i, t instanceof Error ? t : new Error(String(t)));
20
20
  } catch {
21
21
  }
22
- }, j = new Set(o.apps.map((t) => t.appId)), w = (t, e) => typeof t == "string" && t.length > 0 && t.length <= e && !/[\u0000-\u001f\u007f]/.test(t);
22
+ }, j = new Set(r.apps.map((t) => t.appId)), w = (t, e) => typeof t == "string" && t.length > 0 && t.length <= e && !/[\u0000-\u001f\u007f]/.test(t);
23
23
  function m(t) {
24
24
  if (!t || typeof t != "object") return !1;
25
25
  const e = t;
@@ -34,7 +34,7 @@ function J(o, p = {}) {
34
34
  });
35
35
  function $(t) {
36
36
  try {
37
- r.sessionStorage.setItem(
37
+ o.sessionStorage.setItem(
38
38
  v,
39
39
  JSON.stringify({
40
40
  version: 1,
@@ -47,116 +47,118 @@ function J(o, p = {}) {
47
47
  }
48
48
  }
49
49
  function k(t, e = !1) {
50
- try {
51
- const n = t.tabs.find((R) => R.tabId === t.activeTabId), s = n ? x(n) : "#/", a = r.history.state, c = a == null ? void 0 : a[y], d = r.location.hash === s;
52
- if (d && (c == null ? void 0 : c.session) === L && (c == null ? void 0 : c.key) === ((n == null ? void 0 : n.key) ?? null)) return;
53
- const i = `${r.location.pathname}${r.location.search}${s}`, b = {
54
- ...a && typeof a == "object" ? a : {},
55
- [y]: { session: L, key: (n == null ? void 0 : n.key) ?? null }
56
- };
57
- e && !d ? r.history.pushState(b, "", i) : r.history.replaceState(b, "", i);
58
- } catch (n) {
59
- u(n);
60
- }
50
+ if (i.url !== !1)
51
+ try {
52
+ const n = t.tabs.find((R) => R.tabId === t.activeTabId), s = n ? x(n) : "#/", a = o.history.state, d = a == null ? void 0 : a[y], f = o.location.hash === s;
53
+ if (f && (d == null ? void 0 : d.session) === S && (d == null ? void 0 : d.key) === ((n == null ? void 0 : n.key) ?? null)) return;
54
+ const c = `${o.location.pathname}${o.location.search}${s}`, b = {
55
+ ...a && typeof a == "object" ? a : {},
56
+ [y]: { session: S, key: (n == null ? void 0 : n.key) ?? null }
57
+ };
58
+ e && !f ? o.history.pushState(b, "", c) : o.history.replaceState(b, "", c);
59
+ } catch (n) {
60
+ u(n);
61
+ }
61
62
  }
62
63
  function A(t) {
63
- const e = o.getState().tabs.find((s) => s.tabId === t.tabId), n = {
64
+ var s;
65
+ const e = r.getState().tabs.find((a) => a.tabId === t.tabId), n = {
64
66
  ...t,
65
- title: (e == null ? void 0 : e.title) ?? `${t.appId} · ${t.route}`.slice(0, 500)
67
+ title: (e == null ? void 0 : e.title) ?? ((s = i.initialLocation) == null ? void 0 : s.title) ?? `${t.appId} · ${t.route}`.slice(0, 500)
66
68
  };
67
69
  if (!m(n)) throw new Error("URL 包含未知应用或无效页签/路由");
68
- o.openTab(n);
70
+ r.openTab(n);
69
71
  }
70
- let f = null;
72
+ let p = null;
71
73
  try {
72
- f = H(r.location.hash);
74
+ p = i.initialLocation ?? (i.url === !1 ? null : H(o.location.hash));
73
75
  } catch (t) {
74
76
  u(t);
75
77
  }
76
78
  try {
77
- const t = r.sessionStorage.getItem(v);
79
+ const t = o.sessionStorage.getItem(v);
78
80
  if (t) {
79
81
  const e = JSON.parse(t);
80
82
  if (!e || typeof e != "object") throw new Error("无效的页签存储");
81
83
  const n = e;
82
84
  if (n.version !== 1 || !Array.isArray(n.tabs) || n.tabs.length > 100)
83
85
  throw new Error("不支持的页签存储版本或格式");
84
- const s = new Set(o.getState().tabs.map((a) => a.tabId));
86
+ const s = new Set(r.getState().tabs.map((a) => a.tabId));
85
87
  for (const a of n.tabs)
86
- if (!(!m(a) || s.has(a.tabId)) && !(f && m({ ...f, title: "深链恢复" }) && f.tabId === a.tabId && (f.appId !== a.appId || f.route !== a.route)))
88
+ if (!(!m(a) || s.has(a.tabId)) && !(p && m({ ...p, title: "深链恢复" }) && p.tabId === a.tabId && (p.appId !== a.appId || p.route !== a.route)))
87
89
  try {
88
- o.openTab(U(a)), s.add(a.tabId);
89
- } catch (c) {
90
- u(c);
90
+ r.openTab(U(a)), s.add(a.tabId);
91
+ } catch (d) {
92
+ u(d);
91
93
  }
92
- typeof n.activeTabId == "string" && o.activateTab(n.activeTabId);
94
+ typeof n.activeTabId == "string" && r.activateTab(n.activeTabId);
93
95
  }
94
96
  } catch (t) {
95
97
  u(t);
96
98
  }
97
99
  try {
98
- f && A(f);
100
+ p && A(p);
99
101
  } catch (t) {
100
102
  u(t);
101
103
  }
102
- let I = o.getState();
103
- k(I), $(I);
104
+ let l = r.getState();
105
+ k(l), $(l);
104
106
  let E = !1, h = !1;
105
- const K = o.subscribe((t) => {
106
- const e = I.tabs.some((n) => !t.tabs.some((s) => s.key === n.key));
107
- I = t, $(t), E || k(t, !e);
107
+ const K = r.subscribe((t) => {
108
+ const e = l.tabs.some((n) => !t.tabs.some((s) => s.key === n.key));
109
+ l = t, $(t), E || k(t, !e);
108
110
  });
109
- let g = 0, l, S;
111
+ let g = 0, I, L;
110
112
  function T(t) {
111
- var c;
113
+ var d;
112
114
  if (h) return;
113
115
  const e = ++g;
114
- l == null || l.abort();
116
+ I == null || I.abort();
115
117
  const n = new AbortController();
116
- l = n;
117
- const s = r.location.hash, a = (c = r.history.state) == null ? void 0 : c[y];
118
+ I = n;
119
+ const s = o.location.hash, a = (d = o.history.state) == null ? void 0 : d[y];
118
120
  E = !0, (async () => {
119
121
  await Promise.resolve();
120
122
  try {
121
123
  if (h || e !== g) return;
122
- const d = H(s);
123
- if (!d) return;
124
- const i = o.getState().tabs.find((R) => R.tabId === d.tabId), b = {
125
- ...d,
126
- title: (i == null ? void 0 : i.title) ?? `${d.appId} · ${d.route}`.slice(0, 500)
124
+ const f = H(s);
125
+ if (!f) return;
126
+ const c = r.getState().tabs.find((R) => R.tabId === f.tabId), b = {
127
+ ...f,
128
+ title: (c == null ? void 0 : c.title) ?? `${f.appId} · ${f.route}`.slice(0, 500)
127
129
  };
128
130
  if (!m(b)) throw new Error("URL 包含未知应用或无效页签/路由");
129
131
  if (t) {
130
- if ((a == null ? void 0 : a.session) !== L || !i || a.key !== i.key || i.appId !== b.appId || i.route !== b.route)
132
+ if ((a == null ? void 0 : a.session) !== S || !c || a.key !== c.key || c.appId !== b.appId || c.route !== b.route)
131
133
  return;
132
- await o.operate({
134
+ await r.operate({
133
135
  type: "activate",
134
- target: { tabId: i.tabId, key: i.key },
136
+ target: { tabId: c.tabId, key: c.key },
135
137
  signal: n.signal
136
138
  });
137
139
  } else
138
- await o.operate({ type: "open", input: b, signal: n.signal });
139
- } catch (d) {
140
- !h && e === g && u(d);
140
+ await r.operate({ type: "open", input: b, signal: n.signal });
141
+ } catch (f) {
142
+ !h && e === g && u(f);
141
143
  } finally {
142
- !h && e === g && (E = !1, I = o.getState(), k(I), $(I));
144
+ !h && e === g && (E = !1, l = r.getState(), k(l), $(l));
143
145
  }
144
146
  })();
145
147
  }
146
148
  const C = () => {
147
- S = r.location.hash, T(!0);
149
+ L = o.location.hash, T(!0);
148
150
  }, M = (t) => {
149
- const e = t.newURL, n = e ? new URL(e).hash : r.location.hash;
150
- if (S === n) {
151
- S = void 0;
151
+ const e = t.newURL, n = e ? new URL(e).hash : o.location.hash;
152
+ if (L === n) {
153
+ L = void 0;
152
154
  return;
153
155
  }
154
- S = void 0, T(!1);
156
+ L = void 0, T(!1);
155
157
  };
156
- return r.addEventListener("popstate", C), r.addEventListener("hashchange", M), {
158
+ return i.url !== !1 && (o.addEventListener("popstate", C), o.addEventListener("hashchange", M)), {
157
159
  /** 幂等移除本连接器的宿主订阅和浏览器事件;不清空存储、不关闭页签、不销毁宿主。 */
158
160
  dispose() {
159
- h || (h = !0, g++, l == null || l.abort(), K(), r.removeEventListener("popstate", C), r.removeEventListener("hashchange", M));
161
+ h || (h = !0, g++, I == null || I.abort(), K(), i.url !== !1 && (o.removeEventListener("popstate", C), o.removeEventListener("hashchange", M)));
160
162
  }
161
163
  };
162
164
  }
package/dist/core.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const y=require("./sdk.cjs");function Ge(je,z={}){var me;const q=new Map,ye=new Map,R=t=>typeof t=="string"&&!!t.trim()&&!/[\u0000-\u001f\u007f]/.test(t),ce=t=>R(t)&&t.startsWith("/")&&!t.startsWith("//")&&!t.includes("\\"),pe=t=>t.split(/[?#]/,1)[0];for(const t of je){let r=function(c){if(!Array.isArray(c))throw new Error("menus 必须为数组");return Object.freeze(c.map(i=>{if(!i||!R(i.menuId)||!R(i.name)||p.has(i.menuId))throw new Error("菜单标识/名称无效或菜单标识重复");if(p.add(i.menuId),i.children!==void 0){if(i.target!==void 0||!Array.isArray(i.children)||!i.children.length)throw new Error("菜单分组需要非空 children 且不能配置 target");return Object.freeze({menuId:i.menuId,name:i.name,children:r(i.children)})}if(!i.target||!R(i.target.tabId)||!ce(i.target.route))throw new Error("菜单叶节点需要有效 target");return h.add(i.menuId),u.push({pathPrefix:pe(i.target.route),menuId:i.menuId}),Object.freeze({menuId:i.menuId,name:i.name,target:Object.freeze({tabId:i.target.tabId,route:i.target.route})})}))};if(q.has(t.appId))throw new Error(`重复的 appId: ${t.appId}`);const s=new URL(t.entry);if(!["http:","https:"].includes(s.protocol))throw new Error("子应用地址必须使用 HTTP(S)");const p=new Set,h=new Set,u=[],e=t.menus===void 0?void 0:r(t.menus);if(t.menuRoutes!==void 0&&!Array.isArray(t.menuRoutes))throw new Error("menuRoutes 必须为数组");const l=(me=t.menuRoutes)==null?void 0:me.map(c=>{if(!c||!ce(c.pathPrefix)||/[?#]/.test(c.pathPrefix)||!h.has(c.menuId))throw new Error("菜单归属规则需要有效路径与本应用可导航叶节点");return Object.freeze({pathPrefix:c.pathPrefix,menuId:c.menuId})});u.push(...(l??[]).map(c=>({...c})));const a=new Map;for(const c of u){if(c.pathPrefix=c.pathPrefix.replace(/\/+$/,"")||"/",a.has(c.pathPrefix)&&a.get(c.pathPrefix)!==c.menuId)throw new Error("菜单归属前缀冲突");a.set(c.pathPrefix,c.menuId)}ye.set(t.appId,{leaves:h,routes:u.sort((c,i)=>i.pathPrefix.length-c.pathPrefix.length)}),q.set(t.appId,Object.freeze({appId:t.appId,name:t.name,entry:t.entry,...e?{menus:e}:{},...l?{menuRoutes:Object.freeze(l)}:{}}))}const oe=y.createNotificationBus(),Le=Object.freeze({source:"host"}),fe={on(t,r){return oe.on(t,({payload:s,metadata:p})=>B(()=>r(s,p)))},emit(t,r){oe.emit(t,{payload:r,metadata:Le})},clear:()=>oe.clear()},ne=z.callTimeoutMs??3e4;if(!Number.isFinite(ne)||ne<=0||ne>2147483647)throw new Error("callTimeoutMs 必须为不超过2147483647的正有限数");const Y=new Set;function Ie(t,r){if(!t||typeof t.authenticated!="boolean"||!Array.isArray(t.permissions)||!t.permissions.every(s=>typeof s=="string")||t.user!==void 0&&(!t.user||typeof t.user.id!="string"||t.user.name!==void 0&&typeof t.user.name!="string"))throw new Error("会话快照无效");return Object.freeze({authenticated:t.authenticated,permissions:Object.freeze([...t.permissions]),...t.user?{user:Object.freeze({id:t.user.id,...t.user.name!==void 0?{name:t.user.name}:{}})}:{},revision:r})}let J=Ie(z.session??{authenticated:!1,permissions:[]},0);function xe(t){L(),J=Ie(t,J.revision+1);const r=J;for(const s of[...Y]){if(r!==J)break;Y.has(s)&&B(()=>s(r))}return r}const Q=new Set,X=new Map,we=new Map(Object.entries(z.capabilities??{}).filter(([,t])=>t!==void 0).map(([t,r])=>{const s=r;return[t,{handler:s.handler,apps:s.apps?new Set(s.apps):void 0}]})),C=new Map,_=new Map;let f={tabs:[],activeTabId:null},de=0,N=!1,$=0,ve=Promise.resolve();const Z=new Map,F=new Map,ze=t=>{var r;try{(r=z.onError)==null||r.call(z,t)}catch{}},B=t=>{try{t()}catch(r){ze(r)}},L=()=>{if(N)throw new Error("MicroHost 已销毁")},ae=()=>({tabs:[...f.tabs],activeTabId:f.activeTabId}),K=()=>{$++;for(const t of[...Q])Q.has(t)&&B(()=>t(ae()))};function Ee(t){return L(),Q.add(t),()=>{Q.delete(t)}}const le=z.canNavigate,ge=z.canOpen;function U(t){if(!ge)return;const r=Object.freeze({appId:t.appId,tabId:t.tabId,title:t.title,route:t.route,...t.activeMenuId!==void 0?{activeMenuId:t.activeMenuId}:{}});try{const s=ge(r);if(s===!0)return;s&&(typeof s=="object"||typeof s=="function")&&Promise.resolve(s).catch(()=>{})}catch{}throw new y.NavigationError("FORBIDDEN","无权访问该页面(权限策略必须同步返回 true)")}function be(t,r=!1){var e;if(L(),!t||!R(t.title)||t.title.length>500)throw new Error("页签标题无效");if(!q.has(t.appId))throw new Error(`未知子应用: ${t.appId}`);if(!R(t.tabId)||!ce(t.route))throw new Error("需要 tabId 与应用内绝对路由");const s=ye.get(t.appId);if(t.activeMenuId!==void 0&&!s.leaves.has(t.activeMenuId))throw new Error("activeMenuId 必须指向本应用可导航叶节点");const p=pe(t.route),h=t.activeMenuId??((e=s.routes.find(l=>l.pathPrefix==="/"||p===l.pathPrefix||p.startsWith(`${l.pathPrefix}/`)))==null?void 0:e.menuId);let u=f.tabs.find(l=>l.tabId===t.tabId);if(u&&(u.appId!==t.appId||u.route!==t.route&&!r))throw new Error("tabId 已绑定到其他页面");return u&&u.route!==t.route&&(u=void 0),U(u??{...t,activeMenuId:h}),u||(u=Object.freeze({appId:t.appId,tabId:t.tabId,title:t.title,route:t.route,...h!==void 0?{activeMenuId:h}:{},key:`${t.tabId}:${++de}`})),u}function Re(t){const r=be(t);return f={tabs:f.tabs.includes(r)?f.tabs:[...f.tabs,r],activeTabId:r.tabId},K(),r}function He(t,r,s){if(L(),typeof r.title!="string"||!r.title.trim()||r.title.length>500||/[\u0000-\u001f\u007f]/.test(r.title))throw new Error("页签标题无效");const p=f.tabs.find(u=>u.tabId===t&&(!s||u.key===s));if(!p)return;const h=Object.freeze({...p,title:r.title});return f={...f,tabs:f.tabs.map(u=>u===p?h:u)},K(),h}function Ce(t){L();const r=f.tabs.find(s=>s.tabId===t);return r?(U(r),f={...f,activeTabId:t},K(),!0):!1}function _e(t,r){var u;L();const s=f.tabs.findIndex(e=>e.tabId===t&&(!r||e.key===r));if(s<0)return!1;const p=f.tabs[s],h=f.tabs.filter(e=>e!==p);if(f={tabs:h,activeTabId:f.activeTabId===t?((u=h[Math.min(s,h.length-1)])==null?void 0:u.tabId)??null:f.activeTabId},ke(p.key),!h.some(e=>e.appId===p.appId)){for(const e of[..._.get(p.appId)??[]])B(e);_.delete(p.appId),X.delete(p.appId)}return K(),!0}function ke(t){Z.delete(t),F.delete(t);for(const r of[...C.get(t)??[]])B(r);C.delete(t)}function $e(t){return G(t)}function G(t,r,s,p=!1,h,u=!1){try{if(N)throw new y.NavigationError("HOST_DISPOSED","MicroHost 已销毁");if(!t||!["open","activate","refresh","close","closeMany","back"].includes(t.type))throw new y.NavigationError("INVALID_OPERATION","导航命令无效");const e={...t,..."target"in t&&t.target?{target:{...t.target}}:{},..."anchor"in t&&t.anchor?{anchor:{...t.anchor}}:{}},l=o=>!!o&&R(o.tabId)&&(o.key===void 0||R(o.key));if(["activate","refresh","close"].includes(e.type)&&(!("target"in e)||!l(e.target)))throw new y.NavigationError("INVALID_OPERATION","导航需要合法目标引用");if("target"in e&&e.target!==void 0&&!l(e.target)||"anchor"in e&&e.anchor!==void 0&&!l(e.anchor))throw new y.NavigationError("INVALID_OPERATION","导航引用无效");if(e.type==="closeMany"&&e.exclude!==void 0){if(!Array.isArray(e.exclude)||!e.exclude.every(l))throw new y.NavigationError("INVALID_OPERATION","批量排除引用无效");e.exclude=Object.freeze(e.exclude.map(o=>{var k;return Object.freeze({tabId:o.tabId,...o.key!==void 0?{key:o.key}:{key:(k=f.tabs.find(g=>g.tabId===o.tabId))==null?void 0:k.key}})}))}if(e.type==="open"){const o=e.input;if(!o)throw new y.NavigationError("INVALID_OPERATION","导航目标无效");e.input=Object.freeze({appId:o.appId,tabId:o.tabId,title:o.title,route:o.route,...o.activeMenuId!==void 0?{activeMenuId:o.activeMenuId}:{}})}"fallback"in e&&e.fallback&&(e.fallback=Object.freeze({...e.fallback})),"target"in e&&e.target&&Object.freeze(e.target),"anchor"in e&&e.anchor&&Object.freeze(e.anchor),Object.freeze(e);const a=o=>f.tabs.find(k=>o?k.tabId===o.tabId&&(!o.key||k.key===o.key):k.tabId===f.activeTabId),c="target"in e?a(e.target):void 0,i=e.type==="closeMany"&&e.scope!=="all"?e.anchor?a(e.anchor):s?f.tabs.find(o=>o.key===s):a():void 0,T=a(),O=e.type==="open"?f.tabs.find(o=>o.tabId===e.input.tabId):void 0,d=u&&s?f.tabs.find(o=>o.key===s):void 0,I=d&&r?Object.freeze({appId:r,tab:Object.freeze({...d})}):void 0,v=u&&e.type==="open"&&!!r&&e.input.appId!==r,V=()=>{if(!(!v||e.type!=="open")){try{const o=I&&(le==null?void 0:le(I,e.input));if(o===!0)return;o&&(typeof o=="object"||typeof o=="function")&&Promise.resolve(o).catch(()=>{})}catch{}throw new y.NavigationError("FORBIDDEN","无权跨应用导航(权限策略必须同步返回 true)")}},P=o=>!r||o.appId===r;if(c&&!P(c)||i&&!P(i)||e.type==="open"&&r&&e.input.appId!==r&&!v||e.type==="back"&&e.fallback&&r&&e.fallback.appId!==r)throw new y.NavigationError("FORBIDDEN","页面只能操作本应用");if(e.type==="closeMany"&&!["all","others","left","right"].includes(e.scope))throw new y.NavigationError("INVALID_OPERATION","批量范围无效");const ee=i?f.tabs.indexOf(i):-1,W=e.type==="closeMany"?f.tabs.filter((o,k)=>{var g;return P(o)&&!((g=e.exclude)!=null&&g.some(M=>M.tabId===o.tabId&&M.key===o.key))&&(e.scope==="all"||ee>=0&&(e.scope==="others"?o.key!==i.key:e.scope==="left"?k<ee:k>ee))}):[],E=e.type==="back"?c??(!("target"in e)||!e.target?T:void 0):c,se=async()=>{var Oe,Ae,Ne,Te,Pe,Me;const o=n=>({status:n,affected:[]});if(N)throw new y.NavigationError("HOST_DISPOSED","MicroHost 已销毁");if((Oe=e.signal)!=null&&Oe.aborted)return o("stale");if(s&&!p&&(h!=null&&h()||!f.tabs.some(n=>n.key===s)))throw new y.NavigationError("PAGE_RELEASED","页面上下文已释放");if(e.type==="open"&&(O?!f.tabs.some(n=>n.key===O.key):f.tabs.some(n=>n.tabId===e.input.tabId))||E&&!f.tabs.some(n=>n.key===E.key)||i&&!f.tabs.some(n=>n.key===i.key)||W.some(n=>!f.tabs.some(m=>m.key===n.key)))return o("stale");if(e.type!=="open"&&e.type!=="closeMany"&&!E)return o("noop");const k=$;if(V(),s&&!p&&(h!=null&&h()||!f.tabs.some(n=>n.key===s)))throw new y.NavigationError("PAGE_RELEASED","页面上下文已释放");if($!==k||N||(Ae=e.signal)!=null&&Ae.aborted)return o("stale");const g=f,M=$;let w=[...f.tabs],b,S=[],H=f.activeTabId;try{if(e.type==="open"&&(b=be(e.input,!0),O&&O.key!==b.key?(S=[O],w=w.map(n=>n.key===O.key?b:n)):w.some(n=>n.key===b.key)||w.push(b),H=b.tabId),e.type==="activate"&&(b=E,U(b),H=b.tabId),e.type==="refresh"&&(U(E),S=[E],b=Object.freeze({...E,key:`${E.tabId}:${++de}`}),w=w.map(n=>n.key===E.key?b:n)),(e.type==="close"||e.type==="back")&&(S=[E]),e.type==="closeMany"&&(S=W),S.length&&e.type!=="refresh"&&e.type!=="open"){const n=new Set(S.map(D=>D.key)),m=g.tabs.findIndex(D=>D.tabId===g.activeTabId);if(w=w.filter(D=>!n.has(D.key)),e.type==="back"){const D=F.get(E.key);b=w.find(A=>A.key===D&&P(A)),!b&&e.fallback&&(b=be(e.fallback),n.has(b.key)&&(b=Object.freeze({...b,key:`${b.tabId}:${++de}`})),w.some(A=>A.key===b.key)||w.push(b));const j=g.tabs.filter(A=>P(A)),te=j.findIndex(A=>A.key===E.key);b??(b=j.slice(0,te).reverse().find(A=>w.some(re=>re.key===A.key))??j.slice(te+1).find(A=>w.some(re=>re.key===A.key))),b??(b=w[Math.min(m,w.length-1)]),b&&U(b),H=(b==null?void 0:b.tabId)??null}else S.some(D=>D.tabId===H)&&(H=((Ne=w[Math.min(m,w.length-1)])==null?void 0:Ne.tabId)??null)}}catch(n){throw n instanceof y.NavigationError?n:new y.NavigationError("INVALID_OPERATION","导航参数无效")}if(w.length===g.tabs.length&&w.every((n,m)=>n.key===g.tabs[m].key)&&H===g.activeTabId)return o("noop");const ue=[...S],ie=g.tabs.find(n=>n.tabId===g.activeTabId);ie&&ie.tabId!==H&&!ue.some(n=>n.key===ie.key)&&ue.push(ie);for(const n of ue)for(const m of[...Z.get(n.key)??[]]){let D;const j=e.signal;let te=()=>{};try{const A=Promise.resolve(m({operation:e,tab:n})),re=new Promise(De=>{if(!j)return;const Se=()=>De();j.addEventListener("abort",Se,{once:!0}),te=()=>j.removeEventListener("abort",Se),j.aborted&&De()});D=await Promise.race([A,re])}catch{if(j!=null&&j.aborted)return o("stale");throw new y.NavigationError("GUARD_FAILED","离页守卫执行失败")}finally{te()}if($!==M||N||(Te=e.signal)!=null&&Te.aborted)return o("stale");if(D===!1)return o("cancelled")}if($!==M||N||(Pe=e.signal)!=null&&Pe.aborted)return o("stale");if(h!=null&&h())throw new y.NavigationError("PAGE_RELEASED","页面上下文已释放");if(V(),b&&U(b),h!=null&&h())throw new y.NavigationError("PAGE_RELEASED","页面上下文已释放");if($!==M||N||(Me=e.signal)!=null&&Me.aborted)return o("stale");if(f={tabs:w,activeTabId:H},e.type==="open"&&b&&s&&!v&&b.key!==s&&!F.has(b.key)&&F.set(b.key,s),e.type==="refresh"&&b){const n=F.get(E.key);n&&F.set(b.key,n)}for(const n of S)ke(n.key);for(const n of new Set(S.map(m=>m.appId)))if(!w.some(m=>m.appId===n)){for(const m of[..._.get(n)??[]])B(m);_.delete(n),X.delete(n)}const he=[...S,...w.filter(n=>!g.tabs.some(m=>m.key===n.key))];return!he.length&&b&&he.push(b),K(),{status:"committed",affected:he.map(n=>({tabId:n.tabId,key:n.key})),...b?{tab:b}:{}}},x=ve.then(se);return ve=x.catch(()=>{}),x}catch(e){return Promise.reject(e)}}function Fe(t){if(L(),!q.has(t))throw new Error(`未知子应用: ${t}`);const r=X.get(t);if(r)return r;let s=!0;const p=new Set([()=>{s=!1}]);_.set(t,p);async function h(e,l,a,c={}){const i=()=>{if(N)throw new y.HostCallError("HOST_DISPOSED","MicroHost 已销毁");if(a&&(a.released()||!f.tabs.some(P=>P.key===a.key)))throw new y.HostCallError("PAGE_RELEASED","页面上下文已释放");if(!s)throw new y.HostCallError("BRIDGE_RELEASED","应用 bridge 已释放")};i();const T=we.get(e);if(!T)throw new y.HostCallError("UNKNOWN_CAPABILITY",`未注册宿主能力: ${e}`);if(T.apps&&!T.apps.has(t))throw new y.HostCallError("FORBIDDEN",`应用无权调用宿主能力: ${e}`);const O=c.timeoutMs??ne;if(!Number.isFinite(O)||O<=0||O>2147483647)throw new TypeError("timeoutMs 必须为不超过2147483647的正有限数");const d=c.signal;if(d!=null&&d.aborted)throw new y.HostCallError("CANCELLED","宿主能力调用已取消");const I=new AbortController,v=a?f.tabs.find(P=>P.key===a.key):void 0,V=Object.freeze({appId:t,signal:I.signal,...v?{tab:Object.freeze({...v})}:{},isActive:()=>!I.signal.aborted&&!N&&s&&(!a||!a.released()&&f.tabs.some(P=>P.key===a.key))});return new Promise((P,ee)=>{let W=!1,E;const se=()=>{E!==void 0&&clearTimeout(E),d==null||d.removeEventListener("abort",o),p.delete(k),a==null||a.cleanups.delete(k)},x=M=>{W||(W=!0,se(),ee(M),I.abort())},o=()=>x(new y.HostCallError("CANCELLED","宿主能力调用已取消")),k=()=>{try{i()}catch(M){x(M)}};if(p.add(k),a==null||a.cleanups.add(k),d==null||d.addEventListener("abort",o,{once:!0}),E=setTimeout(()=>x(new y.HostCallError("TIMEOUT","宿主能力调用超时")),O),d!=null&&d.aborted){o();return}let g;try{g=T.handler(V,...l)}catch{x(new y.HostCallError("HANDLER_FAILED",`宿主能力执行失败: ${e}`));return}Promise.resolve(g).then(M=>{if(!W){try{i()}catch(w){x(w);return}W=!0,se(),P(M)}},()=>x(new y.HostCallError("HANDLER_FAILED",`宿主能力执行失败: ${e}`)))})}const u={appId:t,getState:ae,callHost(e,...l){return h(e,l)},callHostWithOptions(e,l,...a){return h(e,a,void 0,l)},protocol(){if(L(),!s)throw new Error("应用 bridge 已释放");return Object.freeze({version:"1.0",capabilities:Object.freeze([...we].filter(([,e])=>!e.apps||e.apps.has(t)).map(([e])=>e)),features:Object.freeze(["call-options","session","event-metadata","navigate-to"])})},negotiate(e={}){const l=u.protocol(),a=Object.freeze((e.capabilities??[]).filter(c=>!l.capabilities.includes(c)));return Object.freeze({compatible:(!e.version||e.version===l.version)&&!a.length,protocol:l,missingCapabilities:a})},getSession(){if(L(),!s)throw new Error("应用 bridge 已释放");return J},subscribeSession(e){const l=u.getSession(),a=i=>e(i),c=()=>{Y.delete(a),p.delete(c)};return Y.add(a),p.add(c),B(()=>e(l)),c},subscribe(e){if(!s)throw new Error("应用 bridge 已释放");const l=Ee(e),a=()=>{l(),p.delete(a)};return p.add(a),a},ready(e){const l=c=>{const i=c.tabs.find(T=>T.tabId===c.activeTabId&&T.appId===t);i&&e(i)},a=u.subscribe(l);try{l(ae())}catch(c){throw a(),c}return a},bindPage(e){const l=f.tabs.find(a=>a.tabId===e&&a.appId===t);if(!l)throw new Error("页面不属于该应用");return u.page(l.key)},page(e){if(L(),!s)throw new Error("应用 bridge 已释放");const l=f.tabs.find(d=>d.key===e&&d.appId===t);if(!l)throw new Error("页面上下文不存在或不属于该应用");let a=!1;const c=new Set,i=()=>{if(N)throw new y.NavigationError("HOST_DISPOSED","MicroHost 已销毁");if(a||!f.tabs.some(d=>d.key===e))throw new y.NavigationError("PAGE_RELEASED","页面上下文已释放")},T=()=>{var d;a=!0,c.forEach(I=>I()),c.clear(),(d=C.get(e))==null||d.delete(T)};let O=C.get(e);return O||C.set(e,O=new Set),O.add(T),{get tab(){return f.tabs.find(d=>d.key===e)??l},callHost(d,...I){return h(d,I,{key:e,released:()=>a,cleanups:c})},callHostWithOptions(d,I,...v){return h(d,v,{key:e,released:()=>a,cleanups:c},I)},async openTab(d){i();const I=await G({type:"open",input:d},t,e,!1,()=>a);if(I.tab)return I.tab;const v=f.tabs.find(V=>V.tabId===d.tabId&&V.appId===t);if(I.status==="noop"&&v)return v;throw new y.NavigationError(I.status==="cancelled"?"CANCELLED":"STALE","导航未提交")},async navigateTo(d,I={}){return i(),G({type:"open",input:d,signal:I.signal},t,e,!1,()=>a,!0)},async closeCurrent(){return N?!1:(await G({type:"close",target:{tabId:l.tabId,key:e}},t,void 0,!0)).status==="committed"},beforeLeave(d){if(i(),typeof d!="function")throw new y.NavigationError("INVALID_OPERATION","离页守卫必须为函数");let I=Z.get(e);I||Z.set(e,I=new Set),I.add(d);const v=()=>{I.delete(d),c.delete(v)};return c.add(v),v},async refreshCurrent(){return i(),G({type:"refresh",target:{tabId:l.tabId,key:e}},t,e,!1,()=>a)},async back(d){return i(),G({type:"back",target:{tabId:l.tabId,key:e},fallback:d},t,e,!1,()=>a)},async operate(d){return i(),G(d,t,e,!1,()=>a)},emit(d,I){i(),oe.emit(d,{payload:I,metadata:Object.freeze({source:"app",appId:t,tabKey:e})})},on(d,I){i();const v=fe.on(d,I);return c.add(v),()=>{v(),c.delete(v)}},dispose:T}}};return X.set(t,u),u}function Be(){if(!N){N=!0;for(const t of C.values())[...t].forEach(r=>r());C.clear(),Z.clear(),F.clear();for(const t of _.values())[...t].forEach(r=>r());_.clear(),Q.clear(),Y.clear(),X.clear(),fe.clear(),f={tabs:[],activeTabId:null}}}return{apps:[...q.values()],getState:ae,subscribe:Ee,openTab:Re,activateTab:Ce,closeTab:_e,updateTab:He,updateSession:xe,operate:$e,bridge:Fe,bus:fe,dispose:Be}}exports.createMicroHost=Ge;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const u=require("./sdk.cjs");function Ge(Le,R={}){var Oe;const X=new Map,ye=new Map,z=e=>typeof e=="string"&&!!e.trim()&&!/[\u0000-\u001f\u007f]/.test(e),fe=e=>z(e)&&e.startsWith("/")&&!e.startsWith("//")&&!e.includes("\\"),pe=e=>e.split(/[?#]/,1)[0];for(const e of Le){let a=function(r){if(!Array.isArray(r))throw new Error("menus 必须为数组");return Object.freeze(r.map(i=>{if(!i||!z(i.menuId)||!z(i.name)||c.has(i.menuId))throw new Error("菜单标识/名称无效或菜单标识重复");if(c.add(i.menuId),i.children!==void 0){if(i.target!==void 0||!Array.isArray(i.children)||!i.children.length)throw new Error("菜单分组需要非空 children 且不能配置 target");return Object.freeze({menuId:i.menuId,name:i.name,children:a(i.children)})}if(!i.target||!z(i.target.tabId)||!fe(i.target.route))throw new Error("菜单叶节点需要有效 target");return I.add(i.menuId),b.push({pathPrefix:pe(i.target.route),menuId:i.menuId}),Object.freeze({menuId:i.menuId,name:i.name,target:Object.freeze({tabId:i.target.tabId,route:i.target.route})})}))};if(X.has(e.appId))throw new Error(`重复的 appId: ${e.appId}`);if(e.kind==="local"){if(e.entry!==void 0)throw new Error("本地应用不能配置 entry")}else{if(e.kind!==void 0&&e.kind!=="remote")throw new Error("未知应用类型");const r=new URL(e.entry);if(!["http:","https:"].includes(r.protocol))throw new Error("子应用地址必须使用 HTTP(S)")}const c=new Set,I=new Set,b=[],h=e.menus===void 0?void 0:a(e.menus);if(e.menuRoutes!==void 0&&!Array.isArray(e.menuRoutes))throw new Error("menuRoutes 必须为数组");const o=(Oe=e.menuRoutes)==null?void 0:Oe.map(r=>{if(!r||!fe(r.pathPrefix)||/[?#]/.test(r.pathPrefix)||!I.has(r.menuId))throw new Error("菜单归属规则需要有效路径与本应用可导航叶节点");return Object.freeze({pathPrefix:r.pathPrefix,menuId:r.menuId})});b.push(...(o??[]).map(r=>({...r})));const t=new Map;for(const r of b){if(r.pathPrefix=r.pathPrefix.replace(/\/+$/,"")||"/",t.has(r.pathPrefix)&&t.get(r.pathPrefix)!==r.menuId)throw new Error("菜单归属前缀冲突");t.set(r.pathPrefix,r.menuId)}ye.set(e.appId,{leaves:I,routes:b.sort((r,i)=>i.pathPrefix.length-r.pathPrefix.length)}),X.set(e.appId,Object.freeze({appId:e.appId,name:e.name,...e.kind==="local"?{kind:"local"}:{...e.kind?{kind:e.kind}:{},entry:e.entry},...h?{menus:h}:{},...o?{menuRoutes:Object.freeze(o)}:{}}))}const ae=u.createNotificationBus(),Re=Object.freeze({source:"host"}),de={on(e,a){return ae.on(e,({payload:c,metadata:I})=>U(()=>a(c,I)))},emit(e,a){ae.emit(e,{payload:a,metadata:Re})},clear:()=>ae.clear()},se=R.callTimeoutMs??3e4;if(!Number.isFinite(se)||se<=0||se>2147483647)throw new Error("callTimeoutMs 必须为不超过2147483647的正有限数");const Z=new Set;function we(e,a){if(!e||typeof e.authenticated!="boolean"||!Array.isArray(e.permissions)||!e.permissions.every(c=>typeof c=="string")||e.user!==void 0&&(!e.user||typeof e.user.id!="string"||e.user.name!==void 0&&typeof e.user.name!="string"))throw new Error("会话快照无效");return Object.freeze({authenticated:e.authenticated,permissions:Object.freeze([...e.permissions]),...e.user?{user:Object.freeze({id:e.user.id,...e.user.name!==void 0?{name:e.user.name}:{}})}:{},revision:a})}let K=we(R.session??{authenticated:!1,permissions:[]},0);function ze(e){D(),K=we(e,K.revision+1);const a=K;for(const c of[...Z]){if(a!==K)break;Z.has(c)&&U(()=>c(a))}return a}const ee=new Set,te=new Map,ve=new Map(Object.entries(R.capabilities??{}).filter(([,e])=>e!==void 0).map(([e,a])=>{const c=a;return[e,{handler:c.handler,apps:c.apps?new Set(c.apps):void 0}]})),V=new Map,B=new Map;let d={tabs:[],activeTabId:null},le=0,P=!1,G=0,Ee=Promise.resolve();const re=new Map,W=new Map,xe=e=>{var a;try{(a=R.onError)==null||a.call(R,e)}catch{}},U=e=>{try{e()}catch(a){xe(a)}},D=()=>{if(P)throw new Error("MicroHost 已销毁")},ie=()=>({tabs:[...d.tabs],activeTabId:d.activeTabId}),oe=()=>{G++;for(const e of[...ee])ee.has(e)&&U(()=>e(ie()))};function ke(e){return D(),ee.add(e),()=>{ee.delete(e)}}const be=R.canNavigate,ge=R.canOpen;function q(e){if(!ge)return;const a=Object.freeze({appId:e.appId,tabId:e.tabId,title:e.title,route:e.route,...e.activeMenuId!==void 0?{activeMenuId:e.activeMenuId}:{}});try{const c=ge(a);if(c===!0)return;c&&(typeof c=="object"||typeof c=="function")&&Promise.resolve(c).catch(()=>{})}catch{}throw new u.NavigationError("FORBIDDEN","无权访问该页面(权限策略必须同步返回 true)")}function ue(e,a=!1){var o;if(D(),!e||!z(e.title)||e.title.length>500)throw new Error("页签标题无效");if(!X.has(e.appId))throw new Error(`未知子应用: ${e.appId}`);if(!z(e.tabId)||!fe(e.route))throw new Error("需要 tabId 与应用内绝对路由");const c=ye.get(e.appId);if(e.activeMenuId!==void 0&&!c.leaves.has(e.activeMenuId))throw new Error("activeMenuId 必须指向本应用可导航叶节点");const I=pe(e.route),b=e.activeMenuId??((o=c.routes.find(t=>t.pathPrefix==="/"||I===t.pathPrefix||I.startsWith(`${t.pathPrefix}/`)))==null?void 0:o.menuId);let h=d.tabs.find(t=>t.tabId===e.tabId);if(h&&(h.appId!==e.appId||h.route!==e.route&&!a))throw new Error("tabId 已绑定到其他页面");return h&&h.route!==e.route&&(h=void 0),q(h??{...e,activeMenuId:b}),h||(h=Object.freeze({appId:e.appId,tabId:e.tabId,title:e.title,route:e.route,...b!==void 0?{activeMenuId:b}:{},key:`${e.tabId}:${++le}`})),h}function He(e){const a=ue(e);return d={tabs:d.tabs.includes(a)?d.tabs:[...d.tabs,a],activeTabId:a.tabId},oe(),a}function _e(e,a,c){if(D(),typeof a.title!="string"||!a.title.trim()||a.title.length>500||/[\u0000-\u001f\u007f]/.test(a.title))throw new Error("页签标题无效");const I=d.tabs.find(h=>h.tabId===e&&(!c||h.key===c));if(!I)return;const b=Object.freeze({...I,title:a.title});return d={...d,tabs:d.tabs.map(h=>h===I?b:h)},oe(),b}function Ce(e){D();const a=d.tabs.find(c=>c.tabId===e);return a?(q(a),d={...d,activeTabId:e},oe(),!0):!1}function Fe(e,a){var h;D();const c=d.tabs.findIndex(o=>o.tabId===e&&(!a||o.key===a));if(c<0)return!1;const I=d.tabs[c],b=d.tabs.filter(o=>o!==I);if(d={tabs:b,activeTabId:d.activeTabId===e?((h=b[Math.min(c,b.length-1)])==null?void 0:h.tabId)??null:d.activeTabId},me(I.key),!b.some(o=>o.appId===I.appId)){for(const o of[...B.get(I.appId)??[]])U(o);B.delete(I.appId),te.delete(I.appId)}return oe(),!0}function me(e){re.delete(e),W.delete(e);for(const a of[...V.get(e)??[]])U(a);V.delete(e)}function $e(e){return Y(e)}function Y(e,a,c,I=!1,b,h=!1){var o;try{if(P)throw new u.NavigationError("HOST_DISPOSED","MicroHost 已销毁");if(!e||!["open","activate","refresh","close","closeMany","back"].includes(e.type))throw new u.NavigationError("INVALID_OPERATION","导航命令无效");const t={...e,..."target"in e&&e.target?{target:{...e.target}}:{},..."anchor"in e&&e.anchor?{anchor:{...e.anchor}}:{}},r=n=>!!n&&z(n.tabId)&&(n.key===void 0||z(n.key));if(["activate","refresh","close"].includes(t.type)&&(!("target"in t)||!r(t.target)))throw new u.NavigationError("INVALID_OPERATION","导航需要合法目标引用");if("target"in t&&t.target!==void 0&&!r(t.target)||"anchor"in t&&t.anchor!==void 0&&!r(t.anchor))throw new u.NavigationError("INVALID_OPERATION","导航引用无效");if(t.type==="closeMany"&&t.exclude!==void 0){if(!Array.isArray(t.exclude)||!t.exclude.every(r))throw new u.NavigationError("INVALID_OPERATION","批量排除引用无效");t.exclude=Object.freeze(t.exclude.map(n=>{var m;return Object.freeze({tabId:n.tabId,...n.key!==void 0?{key:n.key}:{key:(m=d.tabs.find(v=>v.tabId===n.tabId))==null?void 0:m.key}})}))}if(t.type==="open"){const n=t.input;if(!n)throw new u.NavigationError("INVALID_OPERATION","导航目标无效");t.input=Object.freeze({appId:n.appId,tabId:n.tabId,title:n.title,route:n.route,...n.activeMenuId!==void 0?{activeMenuId:n.activeMenuId}:{}})}if("closeFallback"in t&&t.closeFallback!==void 0){if(!r(t.closeFallback))throw new u.NavigationError("INVALID_OPERATION","关闭回落引用无效");const n=t.closeFallback;t.closeFallback=Object.freeze({tabId:n.tabId,key:n.key??((o=d.tabs.find(m=>m.tabId===n.tabId))==null?void 0:o.key)})}"fallback"in t&&t.fallback&&(t.fallback=Object.freeze({...t.fallback})),"target"in t&&t.target&&Object.freeze(t.target),"anchor"in t&&t.anchor&&Object.freeze(t.anchor),Object.freeze(t);const i=n=>d.tabs.find(m=>n?m.tabId===n.tabId&&(!n.key||m.key===n.key):m.tabId===d.activeTabId),w="target"in t?i(t.target):void 0,E=t.type==="closeMany"&&t.scope!=="all"?t.anchor?i(t.anchor):c?d.tabs.find(n=>n.key===c):i():void 0,S=i(),f=t.type==="open"?d.tabs.find(n=>n.tabId===t.input.tabId):void 0,y=h&&c?d.tabs.find(n=>n.key===c):void 0,k=y&&a?Object.freeze({appId:a,tab:Object.freeze({...y})}):void 0,x=h&&t.type==="open"&&!!a&&t.input.appId!==a,j=()=>{if(!(!x||t.type!=="open")){try{const n=k&&(be==null?void 0:be(k,t.input));if(n===!0)return;n&&(typeof n=="object"||typeof n=="function")&&Promise.resolve(n).catch(()=>{})}catch{}throw new u.NavigationError("FORBIDDEN","无权跨应用导航(权限策略必须同步返回 true)")}},H=n=>!a||n.appId===a;if(w&&!H(w)||E&&!H(E)||t.type==="open"&&a&&t.input.appId!==a&&!x||t.type==="back"&&t.fallback&&a&&t.fallback.appId!==a)throw new u.NavigationError("FORBIDDEN","页面只能操作本应用");if(t.type==="closeMany"&&!["all","others","left","right"].includes(t.scope))throw new u.NavigationError("INVALID_OPERATION","批量范围无效");const _=E?d.tabs.indexOf(E):-1,J=t.type==="closeMany"?d.tabs.filter((n,m)=>{var v;return H(n)&&!((v=t.exclude)!=null&&v.some(F=>F.tabId===n.tabId&&F.key===n.key))&&(t.scope==="all"||_>=0&&(t.scope==="others"?n.key!==E.key:t.scope==="left"?m<_:m>_))}):[],g=t.type==="back"?w??(!("target"in t)||!t.target?S:void 0):w,C=async()=>{var Ne,Ae,Te,Pe,Me,De;const n=s=>({status:s,affected:[]});if(P)throw new u.NavigationError("HOST_DISPOSED","MicroHost 已销毁");if((Ne=t.signal)!=null&&Ne.aborted)return n("stale");if(c&&!I&&(b!=null&&b()||!d.tabs.some(s=>s.key===c)))throw new u.NavigationError("PAGE_RELEASED","页面上下文已释放");if(t.type==="open"&&(f?!d.tabs.some(s=>s.key===f.key):d.tabs.some(s=>s.tabId===t.input.tabId))||g&&!d.tabs.some(s=>s.key===g.key)||E&&!d.tabs.some(s=>s.key===E.key)||J.some(s=>!d.tabs.some(O=>O.key===s.key)))return n("stale");if(t.type!=="open"&&t.type!=="closeMany"&&!g)return n("noop");const m=G;if(j(),c&&!I&&(b!=null&&b()||!d.tabs.some(s=>s.key===c)))throw new u.NavigationError("PAGE_RELEASED","页面上下文已释放");if(G!==m||P||(Ae=t.signal)!=null&&Ae.aborted)return n("stale");const v=d,F=G;let p=[...d.tabs],l,M=[],$=d.activeTabId;try{if(t.type==="open"&&(l=ue(t.input,!0),f&&f.key!==l.key?(M=[f],p=p.map(s=>s.key===f.key?l:s)):p.some(s=>s.key===l.key)||p.push(l),$=l.tabId),t.type==="activate"&&(l=g,q(l),$=l.tabId),t.type==="refresh"&&(q(g),M=[g],l=Object.freeze({...g,key:`${g.tabId}:${++le}`}),p=p.map(s=>s.key===g.key?l:s)),(t.type==="close"||t.type==="back")&&(M=[g]),t.type==="closeMany"&&(M=J),M.length&&t.type!=="refresh"&&t.type!=="open"){const s=new Set(M.map(N=>N.key)),O=v.tabs.findIndex(N=>N.tabId===v.activeTabId);if(p=p.filter(N=>!s.has(N.key)),t.type==="back"){const N=W.get(g.key);l=p.find(T=>T.key===N&&H(T)),!l&&t.fallback&&(l=ue(t.fallback),s.has(l.key)&&(l=Object.freeze({...l,key:`${l.tabId}:${++le}`})),p.some(T=>T.key===l.key)||p.push(l));const A=v.tabs.filter(T=>H(T)),L=A.findIndex(T=>T.key===g.key);l??(l=A.slice(0,L).reverse().find(T=>p.some(ne=>ne.key===T.key))??A.slice(L+1).find(T=>p.some(ne=>ne.key===T.key))),l??(l=p[Math.min(O,p.length-1)]),l&&q(l),$=(l==null?void 0:l.tabId)??null}else if(M.some(N=>N.tabId===$)){const N="closeFallback"in t?t.closeFallback:void 0,A=N?p.find(L=>L.tabId===N.tabId&&L.key===N.key&&H(L)):void 0;A&&(q(A),l=A),$=((Te=A??p[Math.min(O,p.length-1)])==null?void 0:Te.tabId)??null}}}catch(s){throw s instanceof u.NavigationError?s:new u.NavigationError("INVALID_OPERATION","导航参数无效")}if(p.length===v.tabs.length&&p.every((s,O)=>s.key===v.tabs[O].key)&&$===v.activeTabId)return n("noop");const he=[...M],ce=v.tabs.find(s=>s.tabId===v.activeTabId);ce&&ce.tabId!==$&&!he.some(s=>s.key===ce.key)&&he.push(ce);for(const s of he)for(const O of[...re.get(s.key)??[]]){let N;const A=t.signal;let L=()=>{};try{const T=Promise.resolve(O({operation:t,tab:s})),ne=new Promise(Se=>{if(!A)return;const je=()=>Se();A.addEventListener("abort",je,{once:!0}),L=()=>A.removeEventListener("abort",je),A.aborted&&Se()});N=await Promise.race([T,ne])}catch{if(A!=null&&A.aborted)return n("stale");throw new u.NavigationError("GUARD_FAILED","离页守卫执行失败")}finally{L()}if(G!==F||P||(Pe=t.signal)!=null&&Pe.aborted)return n("stale");if(N===!1)return n("cancelled")}if(G!==F||P||(Me=t.signal)!=null&&Me.aborted)return n("stale");if(b!=null&&b())throw new u.NavigationError("PAGE_RELEASED","页面上下文已释放");if(j(),l&&q(l),b!=null&&b())throw new u.NavigationError("PAGE_RELEASED","页面上下文已释放");if(G!==F||P||(De=t.signal)!=null&&De.aborted)return n("stale");if(d={tabs:p,activeTabId:$},t.type==="open"&&l&&c&&!x&&l.key!==c&&!W.has(l.key)&&W.set(l.key,c),t.type==="refresh"&&l){const s=W.get(g.key);s&&W.set(l.key,s)}for(const s of M)me(s.key);for(const s of new Set(M.map(O=>O.appId)))if(!p.some(O=>O.appId===s)){for(const O of[...B.get(s)??[]])U(O);B.delete(s),te.delete(s)}const Ie=[...M,...p.filter(s=>!v.tabs.some(O=>O.key===s.key))];return!Ie.length&&l&&Ie.push(l),oe(),{status:"committed",affected:Ie.map(s=>({tabId:s.tabId,key:s.key})),...l?{tab:l}:{}}},Q=Ee.then(C);return Ee=Q.catch(()=>{}),Q}catch(t){return Promise.reject(t)}}function Ve(e){if(D(),!X.has(e))throw new Error(`未知子应用: ${e}`);const a=te.get(e);if(a)return a;let c=!0;const I=new Set([()=>{c=!1}]);B.set(e,I);async function b(o,t,r,i={}){const w=()=>{if(P)throw new u.HostCallError("HOST_DISPOSED","MicroHost 已销毁");if(r&&(r.released()||!d.tabs.some(j=>j.key===r.key)))throw new u.HostCallError("PAGE_RELEASED","页面上下文已释放");if(!c)throw new u.HostCallError("BRIDGE_RELEASED","应用 bridge 已释放")};w();const E=ve.get(o);if(!E)throw new u.HostCallError("UNKNOWN_CAPABILITY",`未注册宿主能力: ${o}`);if(E.apps&&!E.apps.has(e))throw new u.HostCallError("FORBIDDEN",`应用无权调用宿主能力: ${o}`);const S=i.timeoutMs??se;if(!Number.isFinite(S)||S<=0||S>2147483647)throw new TypeError("timeoutMs 必须为不超过2147483647的正有限数");const f=i.signal;if(f!=null&&f.aborted)throw new u.HostCallError("CANCELLED","宿主能力调用已取消");const y=new AbortController,k=r?d.tabs.find(j=>j.key===r.key):void 0,x=Object.freeze({appId:e,signal:y.signal,...k?{tab:Object.freeze({...k})}:{},isActive:()=>!y.signal.aborted&&!P&&c&&(!r||!r.released()&&d.tabs.some(j=>j.key===r.key))});return new Promise((j,H)=>{let _=!1,J;const g=()=>{J!==void 0&&clearTimeout(J),f==null||f.removeEventListener("abort",Q),I.delete(n),r==null||r.cleanups.delete(n)},C=v=>{_||(_=!0,g(),H(v),y.abort())},Q=()=>C(new u.HostCallError("CANCELLED","宿主能力调用已取消")),n=()=>{try{w()}catch(v){C(v)}};if(I.add(n),r==null||r.cleanups.add(n),f==null||f.addEventListener("abort",Q,{once:!0}),J=setTimeout(()=>C(new u.HostCallError("TIMEOUT","宿主能力调用超时")),S),f!=null&&f.aborted){Q();return}let m;try{m=E.handler(x,...t)}catch{C(new u.HostCallError("HANDLER_FAILED",`宿主能力执行失败: ${o}`));return}Promise.resolve(m).then(v=>{if(!_){try{w()}catch(F){C(F);return}_=!0,g(),j(v)}},()=>C(new u.HostCallError("HANDLER_FAILED",`宿主能力执行失败: ${o}`)))})}const h={appId:e,getState:ie,callHost(o,...t){return b(o,t)},callHostWithOptions(o,t,...r){return b(o,r,void 0,t)},protocol(){if(D(),!c)throw new Error("应用 bridge 已释放");return Object.freeze({version:"1.0",capabilities:Object.freeze([...ve].filter(([,o])=>!o.apps||o.apps.has(e)).map(([o])=>o)),features:Object.freeze(["call-options","session","event-metadata","navigate-to"])})},negotiate(o={}){const t=h.protocol(),r=Object.freeze((o.capabilities??[]).filter(i=>!t.capabilities.includes(i)));return Object.freeze({compatible:(!o.version||o.version===t.version)&&!r.length,protocol:t,missingCapabilities:r})},getSession(){if(D(),!c)throw new Error("应用 bridge 已释放");return K},subscribeSession(o){const t=h.getSession(),r=w=>o(w),i=()=>{Z.delete(r),I.delete(i)};return Z.add(r),I.add(i),U(()=>o(t)),i},subscribe(o){if(!c)throw new Error("应用 bridge 已释放");const t=ke(o),r=()=>{t(),I.delete(r)};return I.add(r),r},ready(o){const t=i=>{const w=i.tabs.find(E=>E.tabId===i.activeTabId&&E.appId===e);w&&o(w)},r=h.subscribe(t);try{t(ie())}catch(i){throw r(),i}return r},bindPage(o){const t=d.tabs.find(r=>r.tabId===o&&r.appId===e);if(!t)throw new Error("页面不属于该应用");return h.page(t.key)},page(o){if(D(),!c)throw new Error("应用 bridge 已释放");const t=d.tabs.find(f=>f.key===o&&f.appId===e);if(!t)throw new Error("页面上下文不存在或不属于该应用");let r=!1;const i=new Set,w=()=>{if(P)throw new u.NavigationError("HOST_DISPOSED","MicroHost 已销毁");if(r||!d.tabs.some(f=>f.key===o))throw new u.NavigationError("PAGE_RELEASED","页面上下文已释放")},E=()=>{var f;r=!0,i.forEach(y=>y()),i.clear(),(f=V.get(o))==null||f.delete(E)};let S=V.get(o);return S||V.set(o,S=new Set),S.add(E),{get tab(){return d.tabs.find(f=>f.key===o)??t},callHost(f,...y){return b(f,y,{key:o,released:()=>r,cleanups:i})},callHostWithOptions(f,y,...k){return b(f,k,{key:o,released:()=>r,cleanups:i},y)},async openTab(f){w();const y=await Y({type:"open",input:f},e,o,!1,()=>r);if(y.tab)return y.tab;const k=d.tabs.find(x=>x.tabId===f.tabId&&x.appId===e);if(y.status==="noop"&&k)return k;throw new u.NavigationError(y.status==="cancelled"?"CANCELLED":"STALE","导航未提交")},async navigateTo(f,y={}){return w(),Y({type:"open",input:f,signal:y.signal},e,o,!1,()=>r,!0)},async closeCurrent(){return P?!1:(await Y({type:"close",target:{tabId:t.tabId,key:o}},e,void 0,!0)).status==="committed"},beforeLeave(f){if(w(),typeof f!="function")throw new u.NavigationError("INVALID_OPERATION","离页守卫必须为函数");let y=re.get(o);y||re.set(o,y=new Set),y.add(f);const k=()=>{y.delete(f),i.delete(k)};return i.add(k),k},async refreshCurrent(){return w(),Y({type:"refresh",target:{tabId:t.tabId,key:o}},e,o,!1,()=>r)},async back(f){return w(),Y({type:"back",target:{tabId:t.tabId,key:o},fallback:f},e,o,!1,()=>r)},async operate(f){return w(),Y(f,e,o,!1,()=>r)},emit(f,y){w(),ae.emit(f,{payload:y,metadata:Object.freeze({source:"app",appId:e,tabKey:o})})},on(f,y){w();const k=de.on(f,y);return i.add(k),()=>{k(),i.delete(k)}},dispose:E}}};return te.set(e,h),h}function Be(){if(!P){P=!0;for(const e of V.values())[...e].forEach(a=>a());V.clear(),re.clear(),W.clear();for(const e of B.values())[...e].forEach(a=>a());B.clear(),ee.clear(),Z.clear(),te.clear(),de.clear(),d={tabs:[],activeTabId:null}}}return{apps:[...X.values()],getState:ie,subscribe:ke,openTab:He,activateTab:Ce,closeTab:Fe,updateTab:_e,updateSession:ze,operate:$e,bridge:Ve,bus:de,dispose:Be}}exports.createMicroHost=Ge;