@csntgao/uni-base 0.4.0 → 0.5.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.
package/AGENTS.md CHANGED
@@ -37,7 +37,7 @@ examples/vanilla 原生 HTML / JavaScript 接入示例
37
37
 
38
38
  `<uniplat-user-login>` 必须严格复刻亲亲企服 40.1 注册登录原型,提供密码、验证码和扫码三个固定页签、逐字段校验错误态及注册成功过渡页。验证码登录只包含手机号和短信验证码,不得因为接口现状增加图形验证码或其他原型中不存在的字段;旧版 `createCaptcha()` 仅为源码兼容保留,组件不得调用。自定义 Host transport 的密码与扫码方法保持可选,未提供时对应页签保持显示但禁用。UniplatBase 专用 transport 已复用短信验证码及统一 `login-application` 接口提供三种登录能力;组件本身不得保存 JWT,登录结果必须先交给宿主 `setAccessToken()`,再派发不含凭据的成功事件。后端明确返回新用户时显示 2 秒过渡页,并通过 `onboarding-requested` 请求 Host 调用 `<uniplat-account-context>.startOrganizationOnboarding()`。
39
39
 
40
- 组织入驻不是公开 Web Component。40.1 原型 02、A/B 企业/团队创建分支、校验/成功状态及 4.1 无归属提示均由 `<uniplat-account-context>` 内部实现;Host 在其 `organizationOnboarding.transport` 中注入创建能力,不能使用或注册 `<uniplat-organization-onboarding>`。个人用户不允许以非员工身份留在系统里,因此无归属用户必须先入驻,没有任何跳过开关:账户投影为零归属时组件用原生 `dialog.showModal()` 自动打开强制态创建流程,不渲染跳过、无归属提示与关闭按钮,遮罩点击、`Escape` 与 `cancel` 都不关闭。强制态首屏只有创建企业、创建团队和“退出登录”(派发 `logout-requested`)三个出口,不提供邀请码加入入口:加入已有组织通过扫码或粘贴邀请链接进入系统完成,Host 也不需要为强制态实现邀请码页面。组织邀请页豁免强制入驻:当前 URL 末段路径命中 `organizationOnboarding.invitePaths`(默认 `['org-invite']`)且带非空 `code` 时不弹对话框,避免盖住加入流程;路径与 hash 路由都参与匹配,组件只取一个布尔值,邀请码的值不得进入组件状态、DOM、公共事件或日志。离开邀请页(`popstate`/`hashchange`)后仍无员工身份时必须重新强制入驻,不得留下可浏览的无归属态。企业/团队表单页提供“返回”回到创建方式选择,创建成功页的认证、升级或邀请动作才是关闭对话框的终点。`organizationOnboarding` 只接收 `transport`,不得再提供 `mandatory` 之类的退出开关。强制态必须使用 top layer 对话框,不得只依赖可能被 Host `transform/overflow` 限制的固定遮罩。点击账户菜单或个人态的“创建团队 / 企业”必须直接启动内部流程,不得向 Host 派发 `organization-create-requested` 或 `create-organization-requested`。真实 UniplatBase transport 复用 `createUniplatBaseOrganizationOnboardingTransport()` 与 `system.org/create_organization`。邀请码加入、企业认证、团队升级和邀请成员等未展开页面仍派发业务事件,由 Host 实现。
40
+ 组织入驻不是公开 Web Component。40.1 原型 02、A/B 企业/团队创建分支、校验/成功状态及 4.1 无归属提示均由 `<uniplat-account-context>` 内部实现;Host 在其 `organizationOnboarding.transport` 中注入创建能力,不能使用或注册 `<uniplat-organization-onboarding>`。入驻流程一律用原生 `dialog.showModal()` 在视口居中呈现,不再使用锚定账户胶囊的下拉浮层;强制态与普通态只在能否关闭上不同,普通态可由遮罩点击、`Escape` 或“取消”收起,关闭时必须同步清空面板状态,避免 `dialog` 与组件状态不一致。右上角的“取消”只在当前用户已归属某个组织时渲染(内部 `canCancel`,由 Account Context 按投影中的归属数量传入),无归属用户不显示取消,点击后直接收起流程,不再经过“确定暂不创建”的确认页。个人用户不允许以非员工身份留在系统里,因此无归属用户必须先入驻,没有任何跳过开关:账户投影为零归属时组件用原生 `dialog.showModal()` 自动打开强制态创建流程,不渲染跳过、无归属提示与关闭按钮,遮罩点击、`Escape` 与 `cancel` 都不关闭。强制态首屏只有创建企业、创建团队和“退出登录”(派发 `logout-requested`)三个出口,不提供邀请码加入入口:加入已有组织通过扫码或粘贴邀请链接进入系统完成,Host 也不需要为强制态实现邀请码页面。组织邀请页豁免强制入驻:当前 URL 末段路径命中 `organizationOnboarding.invitePaths`(默认 `['org-invite']`)且带非空 `code` 时不弹对话框,避免盖住加入流程;路径与 hash 路由都参与匹配,组件只取一个布尔值,邀请码的值不得进入组件状态、DOM、公共事件或日志。离开邀请页(`popstate`/`hashchange`)后仍无员工身份时必须重新强制入驻,不得留下可浏览的无归属态。企业/团队表单页提供“返回”回到创建方式选择,创建成功页的认证、升级或邀请动作才是关闭对话框的终点。`organizationOnboarding` 只接收 `transport`,不得再提供 `mandatory` 之类的退出开关。强制态必须使用 top layer 对话框,不得只依赖可能被 Host `transform/overflow` 限制的固定遮罩。点击账户菜单或个人态的“创建团队 / 企业”必须直接启动内部流程,不得向 Host 派发 `organization-create-requested` 或 `create-organization-requested`。真实 UniplatBase transport 复用 `createUniplatBaseOrganizationOnboardingTransport()` 与 `system.org/create_organization`。邀请码加入、企业认证、团队升级和邀请成员等未展开页面仍派发业务事件,由 Host 实现。
41
41
 
42
42
  `<uniplat-organization-selector>` 覆盖 40.1 原型 S.1 多归属选择弹窗,显示 Host 提供的完整企业/团队列表、类型、认证状态、角色、成员数和最近进入信息,并自行负责脱敏账户摘要、名称与统一社会信用代码搜索、全部/企业/团队类型筛选、“最近进入 / 全部组织”分组、默认选中和带目标名称的主按钮。选中与记住偏好只在组件内临时维护,点击“进入”后交由 Host 完成登录态切换,“退出登录”只派发事件。`presentation: 'modal'` 时组件自渲染 S.1 强制态弹窗:固定遮罩、无关闭按钮,遮罩点击与 `Escape` 都不关闭,`Escape` 也不冒泡到 Host 的全局关闭逻辑;默认 `inline` 只输出卡片,遮罩归 Host。S.0 的“弹不弹”判定由纯函数 `resolveOrganizationEntryDecision()` 提供,只按 0 个归属、1 个归属、命中记住选择、2 个及以上分别返回 `personal | auto-enter | prompt`;它不读写宿主存储,记住偏好由 Host 自行持久化并回传,记住的组织已不在列表时退回 `prompt`。
43
43
 
@@ -439,7 +439,7 @@ npm 包名、导出类型名、Custom Element 标签、`configure()` 结构、
439
439
  - Organization Selector 需测试完整列表、企业认证与团队类型样式、脱敏账户摘要与完整手机号拒绝、按名称和统一社会信用代码搜索且代码不进入 DOM、类型筛选与常显计数、“最近进入 / 全部组织”分组与默认选中、带目标名称的主按钮、无匹配与空归属状态、记住偏好、创建/进入/退出登录事件、`modal` 强制态无关闭按钮且遮罩点击与 `Escape` 都不关闭并在移除后解除 `Escape` 拦截、非法 `presentation` 与重复 ID 拒绝和普通文本插值;事件只包含稳定组织 ID 与偏好,不含姓名、手机号、组织名称或信用代码。`resolveOrganizationEntryDecision()` 需测试 0 个归属、单一归属、命中与失效的记住选择、2 个及以上的默认选中项和非法输入拒绝。
440
440
  - Organization Multi Employee Selector 需测试部门树、员工分页和搜索、初始选择补取、跨部门按员工 ID 去重、多选上限、受保护项、确认/取消事件、独立滚动、移动视图、错误重试、身份切换清理,以及 JWT、完整手机号、身份字段和后端错误不进入 DOM、事件、URL或日志。专用 transport 必须测试每次请求重新读取 JWT、只使用 Authorization header、组织级 directory scope 和无调用方身份参数。
441
441
  - Organization Single Employee Selector 需测试独立标签与导出、初始单个 ID 回显、新选择替换旧选择、未选时禁止确认、受保护项、标量确认 detail、拒绝多选打开参数,以及与多选组件相同的数据和认证安全边界。
442
- - Account Context 需测试必填应用名、组件通过 transport 自行加载用户资料与完整应用组织投影、无归属时仍渲染账户胶囊但不显示工作台入口、带 `code` 的邀请页豁免强制入驻且邀请码不进 DOM、缺 `code` 或其他路径仍强制、离开邀请页后重新强制、Host 自定义 `invitePaths` 生效、企业五种认证状态和团队状态、当前组织员工身份优先及无组织用户身份回退、安全头像与加载失败回退、胶囊内姓名 6 字与组织名 8 字截断及完整 `title`、未超长时不加 `title`、工作台、账户菜单与内置入驻浮层互斥、浮层不改变导航布局、内嵌应用九宫格、创建入口直接启动内置入驻、Host 响应组织切换事件、退出和公共事件安全。专用 UniplatBase transport 还必须测试不请求 `profile_summary_v2`、从 `list_available_organizations` 顶层读取用户资料、按 `application_name` 加载投影、Open Application 的 `organization_oid` 切换、组织级应用目标 `xid` 选择、JWT 只进入 Authorization header 和 Host `setAccessToken()`、响应净化、稳定错误以及公共数据无 JWT;组件范围不得扩展为品牌或完整导航。
442
+ - Account Context 需测试入驻流程始终渲染为居中的原生对话框而非下拉浮层、普通态可由遮罩点击关闭且强制态不可、已归属用户显示“取消”而无归属用户不显示、必填应用名、组件通过 transport 自行加载用户资料与完整应用组织投影、无归属时仍渲染账户胶囊但不显示工作台入口、带 `code` 的邀请页豁免强制入驻且邀请码不进 DOM、缺 `code` 或其他路径仍强制、离开邀请页后重新强制、Host 自定义 `invitePaths` 生效、企业五种认证状态和团队状态、当前组织员工身份优先及无组织用户身份回退、安全头像与加载失败回退、胶囊内姓名 6 字与组织名 8 字截断及完整 `title`、未超长时不加 `title`、工作台、账户菜单与内置入驻浮层互斥、浮层不改变导航布局、内嵌应用九宫格、创建入口直接启动内置入驻、Host 响应组织切换事件、退出和公共事件安全。专用 UniplatBase transport 还必须测试不请求 `profile_summary_v2`、从 `list_available_organizations` 顶层读取用户资料、按 `application_name` 加载投影、Open Application 的 `organization_oid` 切换、组织级应用目标 `xid` 选择、JWT 只进入 Authorization header 和 Host `setAccessToken()`、响应净化、稳定错误以及公共数据无 JWT;组件范围不得扩展为品牌或完整导航。
443
443
  - 通知中心需测试组件注册与导出、两栏首版输出、数据驱动分类页签(`includeTypeFacets` 随收件箱请求、后端 `type_facets` 优先、无 facet 回退累积、筛选后页签不塌缩、展示名取 `notification_type_name` 且为空时回退原始 code、契约四个码不得残留内置中文名、实时新分类入目录)、未读角标与 `unread-count-change`、打开未读即标记已读、全部已读、加载更多、关联资源事件只含安全业务标识、附件经 transport 下载、Markdown 净化、`applyRealtimeEvent()` 不建立任何 WebSocket 且原地更新列表与未读数、事件无法映射时降级刷新、收件箱在途时延后 resync 不丢事件、`configure()` 之前与移除之后喂入事件无副作用、错误横幅为净化文案,以及 JWT 与后端原始正文不进入 DOM、公共事件或日志。小型通知浮层还需测试初始关闭、4 条未读投影、未读总数、严格原型结构与 SVG 图标、单条/全部已读、完整通知页意图事件只含稳定 ID、外部关闭、空态以及 Host 实时事件更新。
444
444
  - 使用 fake timer 的测试必须恢复 timer;构造 fake WebSocket 时不要把测试凭据写入 URL。
445
445
  - 修复竞态或生命周期问题时增加回归测试,不只验证最终 DOM。
package/CLAUDE.md CHANGED
@@ -37,7 +37,7 @@ examples/vanilla 原生 HTML / JavaScript 接入示例
37
37
 
38
38
  `<uniplat-user-login>` 必须严格复刻亲亲企服 40.1 注册登录原型,提供密码、验证码和扫码三个固定页签、逐字段校验错误态及注册成功过渡页。验证码登录只包含手机号和短信验证码,不得因为接口现状增加图形验证码或其他原型中不存在的字段;旧版 `createCaptcha()` 仅为源码兼容保留,组件不得调用。自定义 Host transport 的密码与扫码方法保持可选,未提供时对应页签保持显示但禁用。UniplatBase 专用 transport 已复用短信验证码及统一 `login-application` 接口提供三种登录能力;组件本身不得保存 JWT,登录结果必须先交给宿主 `setAccessToken()`,再派发不含凭据的成功事件。后端明确返回新用户时显示 2 秒过渡页,并通过 `onboarding-requested` 请求 Host 调用 `<uniplat-account-context>.startOrganizationOnboarding()`。
39
39
 
40
- 组织入驻不是公开 Web Component。40.1 原型 02、A/B 企业/团队创建分支、校验/成功状态及 4.1 无归属提示均由 `<uniplat-account-context>` 内部实现;Host 在其 `organizationOnboarding.transport` 中注入创建能力,不能使用或注册 `<uniplat-organization-onboarding>`。个人用户不允许以非员工身份留在系统里,因此无归属用户必须先入驻,没有任何跳过开关:账户投影为零归属时组件用原生 `dialog.showModal()` 自动打开强制态创建流程,不渲染跳过、无归属提示与关闭按钮,遮罩点击、`Escape` 与 `cancel` 都不关闭。强制态首屏只有创建企业、创建团队和“退出登录”(派发 `logout-requested`)三个出口,不提供邀请码加入入口:加入已有组织通过扫码或粘贴邀请链接进入系统完成,Host 也不需要为强制态实现邀请码页面。组织邀请页豁免强制入驻:当前 URL 末段路径命中 `organizationOnboarding.invitePaths`(默认 `['org-invite']`)且带非空 `code` 时不弹对话框,避免盖住加入流程;路径与 hash 路由都参与匹配,组件只取一个布尔值,邀请码的值不得进入组件状态、DOM、公共事件或日志。离开邀请页(`popstate`/`hashchange`)后仍无员工身份时必须重新强制入驻,不得留下可浏览的无归属态。企业/团队表单页提供“返回”回到创建方式选择,创建成功页的认证、升级或邀请动作才是关闭对话框的终点。`organizationOnboarding` 只接收 `transport`,不得再提供 `mandatory` 之类的退出开关。强制态必须使用 top layer 对话框,不得只依赖可能被 Host `transform/overflow` 限制的固定遮罩。点击账户菜单或个人态的“创建团队 / 企业”必须直接启动内部流程,不得向 Host 派发 `organization-create-requested` 或 `create-organization-requested`。真实 UniplatBase transport 复用 `createUniplatBaseOrganizationOnboardingTransport()` 与 `system.org/create_organization`。邀请码加入、企业认证、团队升级和邀请成员等未展开页面仍派发业务事件,由 Host 实现。
40
+ 组织入驻不是公开 Web Component。40.1 原型 02、A/B 企业/团队创建分支、校验/成功状态及 4.1 无归属提示均由 `<uniplat-account-context>` 内部实现;Host 在其 `organizationOnboarding.transport` 中注入创建能力,不能使用或注册 `<uniplat-organization-onboarding>`。入驻流程一律用原生 `dialog.showModal()` 在视口居中呈现,不再使用锚定账户胶囊的下拉浮层;强制态与普通态只在能否关闭上不同,普通态可由遮罩点击、`Escape` 或“取消”收起,关闭时必须同步清空面板状态,避免 `dialog` 与组件状态不一致。右上角的“取消”只在当前用户已归属某个组织时渲染(内部 `canCancel`,由 Account Context 按投影中的归属数量传入),无归属用户不显示取消,点击后直接收起流程,不再经过“确定暂不创建”的确认页。个人用户不允许以非员工身份留在系统里,因此无归属用户必须先入驻,没有任何跳过开关:账户投影为零归属时组件用原生 `dialog.showModal()` 自动打开强制态创建流程,不渲染跳过、无归属提示与关闭按钮,遮罩点击、`Escape` 与 `cancel` 都不关闭。强制态首屏只有创建企业、创建团队和“退出登录”(派发 `logout-requested`)三个出口,不提供邀请码加入入口:加入已有组织通过扫码或粘贴邀请链接进入系统完成,Host 也不需要为强制态实现邀请码页面。组织邀请页豁免强制入驻:当前 URL 末段路径命中 `organizationOnboarding.invitePaths`(默认 `['org-invite']`)且带非空 `code` 时不弹对话框,避免盖住加入流程;路径与 hash 路由都参与匹配,组件只取一个布尔值,邀请码的值不得进入组件状态、DOM、公共事件或日志。离开邀请页(`popstate`/`hashchange`)后仍无员工身份时必须重新强制入驻,不得留下可浏览的无归属态。企业/团队表单页提供“返回”回到创建方式选择,创建成功页的认证、升级或邀请动作才是关闭对话框的终点。`organizationOnboarding` 只接收 `transport`,不得再提供 `mandatory` 之类的退出开关。强制态必须使用 top layer 对话框,不得只依赖可能被 Host `transform/overflow` 限制的固定遮罩。点击账户菜单或个人态的“创建团队 / 企业”必须直接启动内部流程,不得向 Host 派发 `organization-create-requested` 或 `create-organization-requested`。真实 UniplatBase transport 复用 `createUniplatBaseOrganizationOnboardingTransport()` 与 `system.org/create_organization`。邀请码加入、企业认证、团队升级和邀请成员等未展开页面仍派发业务事件,由 Host 实现。
41
41
 
42
42
  `<uniplat-organization-selector>` 覆盖 40.1 原型 S.1 多归属选择弹窗,显示 Host 提供的完整企业/团队列表、类型、认证状态、角色、成员数和最近进入信息,并自行负责脱敏账户摘要、名称与统一社会信用代码搜索、全部/企业/团队类型筛选、“最近进入 / 全部组织”分组、默认选中和带目标名称的主按钮。选中与记住偏好只在组件内临时维护,点击“进入”后交由 Host 完成登录态切换,“退出登录”只派发事件。`presentation: 'modal'` 时组件自渲染 S.1 强制态弹窗:固定遮罩、无关闭按钮,遮罩点击与 `Escape` 都不关闭,`Escape` 也不冒泡到 Host 的全局关闭逻辑;默认 `inline` 只输出卡片,遮罩归 Host。S.0 的“弹不弹”判定由纯函数 `resolveOrganizationEntryDecision()` 提供,只按 0 个归属、1 个归属、命中记住选择、2 个及以上分别返回 `personal | auto-enter | prompt`;它不读写宿主存储,记住偏好由 Host 自行持久化并回传,记住的组织已不在列表时退回 `prompt`。
43
43
 
@@ -439,7 +439,7 @@ npm 包名、导出类型名、Custom Element 标签、`configure()` 结构、
439
439
  - Organization Selector 需测试完整列表、企业认证与团队类型样式、脱敏账户摘要与完整手机号拒绝、按名称和统一社会信用代码搜索且代码不进入 DOM、类型筛选与常显计数、“最近进入 / 全部组织”分组与默认选中、带目标名称的主按钮、无匹配与空归属状态、记住偏好、创建/进入/退出登录事件、`modal` 强制态无关闭按钮且遮罩点击与 `Escape` 都不关闭并在移除后解除 `Escape` 拦截、非法 `presentation` 与重复 ID 拒绝和普通文本插值;事件只包含稳定组织 ID 与偏好,不含姓名、手机号、组织名称或信用代码。`resolveOrganizationEntryDecision()` 需测试 0 个归属、单一归属、命中与失效的记住选择、2 个及以上的默认选中项和非法输入拒绝。
440
440
  - Organization Multi Employee Selector 需测试部门树、员工分页和搜索、初始选择补取、跨部门按员工 ID 去重、多选上限、受保护项、确认/取消事件、独立滚动、移动视图、错误重试、身份切换清理,以及 JWT、完整手机号、身份字段和后端错误不进入 DOM、事件、URL或日志。专用 transport 必须测试每次请求重新读取 JWT、只使用 Authorization header、组织级 directory scope 和无调用方身份参数。
441
441
  - Organization Single Employee Selector 需测试独立标签与导出、初始单个 ID 回显、新选择替换旧选择、未选时禁止确认、受保护项、标量确认 detail、拒绝多选打开参数,以及与多选组件相同的数据和认证安全边界。
442
- - Account Context 需测试必填应用名、组件通过 transport 自行加载用户资料与完整应用组织投影、无归属时仍渲染账户胶囊但不显示工作台入口、带 `code` 的邀请页豁免强制入驻且邀请码不进 DOM、缺 `code` 或其他路径仍强制、离开邀请页后重新强制、Host 自定义 `invitePaths` 生效、企业五种认证状态和团队状态、当前组织员工身份优先及无组织用户身份回退、安全头像与加载失败回退、胶囊内姓名 6 字与组织名 8 字截断及完整 `title`、未超长时不加 `title`、工作台、账户菜单与内置入驻浮层互斥、浮层不改变导航布局、内嵌应用九宫格、创建入口直接启动内置入驻、Host 响应组织切换事件、退出和公共事件安全。专用 UniplatBase transport 还必须测试不请求 `profile_summary_v2`、从 `list_available_organizations` 顶层读取用户资料、按 `application_name` 加载投影、Open Application 的 `organization_oid` 切换、组织级应用目标 `xid` 选择、JWT 只进入 Authorization header 和 Host `setAccessToken()`、响应净化、稳定错误以及公共数据无 JWT;组件范围不得扩展为品牌或完整导航。
442
+ - Account Context 需测试入驻流程始终渲染为居中的原生对话框而非下拉浮层、普通态可由遮罩点击关闭且强制态不可、已归属用户显示“取消”而无归属用户不显示、必填应用名、组件通过 transport 自行加载用户资料与完整应用组织投影、无归属时仍渲染账户胶囊但不显示工作台入口、带 `code` 的邀请页豁免强制入驻且邀请码不进 DOM、缺 `code` 或其他路径仍强制、离开邀请页后重新强制、Host 自定义 `invitePaths` 生效、企业五种认证状态和团队状态、当前组织员工身份优先及无组织用户身份回退、安全头像与加载失败回退、胶囊内姓名 6 字与组织名 8 字截断及完整 `title`、未超长时不加 `title`、工作台、账户菜单与内置入驻浮层互斥、浮层不改变导航布局、内嵌应用九宫格、创建入口直接启动内置入驻、Host 响应组织切换事件、退出和公共事件安全。专用 UniplatBase transport 还必须测试不请求 `profile_summary_v2`、从 `list_available_organizations` 顶层读取用户资料、按 `application_name` 加载投影、Open Application 的 `organization_oid` 切换、组织级应用目标 `xid` 选择、JWT 只进入 Authorization header 和 Host `setAccessToken()`、响应净化、稳定错误以及公共数据无 JWT;组件范围不得扩展为品牌或完整导航。
443
443
  - 通知中心需测试组件注册与导出、两栏首版输出、数据驱动分类页签(`includeTypeFacets` 随收件箱请求、后端 `type_facets` 优先、无 facet 回退累积、筛选后页签不塌缩、展示名取 `notification_type_name` 且为空时回退原始 code、契约四个码不得残留内置中文名、实时新分类入目录)、未读角标与 `unread-count-change`、打开未读即标记已读、全部已读、加载更多、关联资源事件只含安全业务标识、附件经 transport 下载、Markdown 净化、`applyRealtimeEvent()` 不建立任何 WebSocket 且原地更新列表与未读数、事件无法映射时降级刷新、收件箱在途时延后 resync 不丢事件、`configure()` 之前与移除之后喂入事件无副作用、错误横幅为净化文案,以及 JWT 与后端原始正文不进入 DOM、公共事件或日志。小型通知浮层还需测试初始关闭、4 条未读投影、未读总数、严格原型结构与 SVG 图标、单条/全部已读、完整通知页意图事件只含稳定 ID、外部关闭、空态以及 Host 实时事件更新。
444
444
  - 使用 fake timer 的测试必须恢复 timer;构造 fake WebSocket 时不要把测试凭据写入 URL。
445
445
  - 修复竞态或生命周期问题时增加回归测试,不只验证最终 DOM。
package/README.md CHANGED
@@ -298,6 +298,8 @@ accountContext.configure({
298
298
 
299
299
  该 transport 复用 `POST system.org/create_organization`。企业请求映射为 `organization_type/team_name/customer_name/credit_code`,团队请求只发送 `organization_type/team_name`;不会提交 `uid/oid/eid/owner` 等身份字段。它在每次创建前重新调用 `getAccessToken()`,JWT 只进入 `Authorization: Bearer` header,成功响应只保留组件需要的组织 ID、展示名称和类型。`INVALID_ARGUMENT`、`ORGANIZATION_CREATE_FAILED`、HTTP、解析及 token provider 异常均收敛为 `UNKNOWN`,不透传后端正文。
300
300
 
301
+ 入驻流程一律以原生 `dialog.showModal()` 在视口居中呈现(top layer,不受 Host 导航 `transform`/`overflow` 影响),不再使用锚定账户胶囊的下拉浮层;账户菜单里点“创建团队 / 企业”打开的普通态同样居中,可由遮罩点击、`Escape` 或右上角“取消”关闭。“取消”只在当前用户已归属某个组织时渲染,无归属用户看不到该按钮;点击后直接收起流程,不再经过“确定暂不创建”确认页。
302
+
301
303
  内部流程会经账户组件派发 `onboarding-skipped`、`join-organization-requested`、`organization-created`、`enterprise-authentication-requested`、`enterprise-authentication-deferred`、`team-upgrade-requested`、`team-invite-members-requested`、`logout-requested` 和安全的 `error` 事件。Host 可通过账户组件的 `onboarding-panel` part 定制该浮层或对话框。
302
304
 
303
305
  个人用户不允许以非员工身份留在系统里,所以无归属用户必须先入驻,没有跳过开关。账户投影返回零归属时,组件用原生 `dialog.showModal()` 自动打开强制态创建流程:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@csntgao/uni-base",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "packageManager": "pnpm@11.3.0",
6
6
  "publishConfig": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniplat/ai-employee-web-components",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Framework-independent Web Components for Uniplat account, login, application switching and AI Employee chat",
5
5
  "type": "module",
6
6
  "sideEffects": true,
@@ -459,10 +459,6 @@ export class UniplatAccountContext extends LitElement {
459
459
  width: min(328px, calc(100vw - 24px));
460
460
  }
461
461
 
462
- .onboarding-panel {
463
- width: min(400px, calc(100vw - 24px));
464
- }
465
-
466
462
  /*
467
463
  * 强制入驻使用原生 dialog 的 top layer,不受 Host 导航 transform / overflow 影响;
468
464
  * 无关闭按钮,遮罩点击与 Escape 都不关闭。
@@ -488,7 +484,7 @@ export class UniplatAccountContext extends LitElement {
488
484
  background: rgb(20 35 56 / 56%);
489
485
  }
490
486
 
491
- .onboarding-modal uniplat-internal-organization-onboarding {
487
+ .onboarding-dialog {
492
488
  width: min(400px, 100%);
493
489
  }
494
490
 
@@ -1079,6 +1075,9 @@ export class UniplatAccountContext extends LitElement {
1079
1075
  transport,
1080
1076
  initialStep: 'method-selection',
1081
1077
  mandatory: this.mandatoryOnboarding,
1078
+ // 取消只在用户已归属某个组织时提供;配置只在面板打开时取一次,
1079
+ // 创建成功后不重新配置,避免把成功页重置回创建方式选择。
1080
+ canCancel: (this.viewModel?.organizations.length ?? 0) > 0,
1082
1081
  })
1083
1082
  this.configuredOrganizationOnboarding = onboarding
1084
1083
  this.configuredOnboardingMandatory = this.mandatoryOnboarding
@@ -1151,31 +1150,24 @@ export class UniplatAccountContext extends LitElement {
1151
1150
  </div>`
1152
1151
  }
1153
1152
  if (this.activePanel === 'onboarding' && this.organizationOnboardingTransport) {
1154
- const onboarding = html`<uniplat-internal-organization-onboarding></uniplat-internal-organization-onboarding>`
1155
- if (this.mandatoryOnboarding) {
1156
- return html`<dialog
1157
- class="onboarding-modal"
1158
- part="onboarding-panel"
1159
- aria-label="创建团队或企业"
1160
- @cancel=${this.blockMandatoryOnboardingCancel}
1161
- @onboarding-skipped=${this.handleOnboardingSkipped}
1162
- @organization-created=${this.refreshAfterOrganizationCreated}
1163
- @enterprise-authentication-requested=${this.finishMandatoryOnboarding}
1164
- @enterprise-authentication-deferred=${this.finishMandatoryOnboarding}
1165
- @team-upgrade-requested=${this.finishMandatoryOnboarding}
1166
- @team-invite-members-requested=${this.finishMandatoryOnboarding}
1167
- >
1168
- ${onboarding}
1169
- </dialog>`
1170
- }
1171
- return html`<div
1172
- class="floating-panel onboarding-panel"
1153
+ // 入驻流程始终以视口居中的原生对话框呈现;强制态与普通态只在能否关闭上不同。
1154
+ return html`<dialog
1155
+ class="onboarding-modal"
1173
1156
  part="onboarding-panel"
1157
+ aria-label="创建团队或企业"
1158
+ @click=${this.closeOnboardingFromBackdrop}
1159
+ @cancel=${this.handleOnboardingCancel}
1174
1160
  @onboarding-skipped=${this.handleOnboardingSkipped}
1175
1161
  @organization-created=${this.refreshAfterOrganizationCreated}
1162
+ @enterprise-authentication-requested=${this.finishMandatoryOnboarding}
1163
+ @enterprise-authentication-deferred=${this.finishMandatoryOnboarding}
1164
+ @team-upgrade-requested=${this.finishMandatoryOnboarding}
1165
+ @team-invite-members-requested=${this.finishMandatoryOnboarding}
1176
1166
  >
1177
- ${onboarding}
1178
- </div>`
1167
+ <div class="onboarding-dialog" @click=${stopEventPropagation}>
1168
+ <uniplat-internal-organization-onboarding></uniplat-internal-organization-onboarding>
1169
+ </div>
1170
+ </dialog>`
1179
1171
  }
1180
1172
  if (this.activePanel === 'account') {
1181
1173
  return html`<div
@@ -1325,9 +1317,16 @@ export class UniplatAccountContext extends LitElement {
1325
1317
  this.activePanel = null
1326
1318
  }
1327
1319
 
1328
- private readonly blockMandatoryOnboardingCancel = (event: Event): void => {
1329
- // 强制入驻不允许 Escape 关闭原生对话框。
1330
- if (this.onboardingLocked) event.preventDefault()
1320
+ private readonly handleOnboardingCancel = (event: Event): void => {
1321
+ // 强制入驻不允许 Escape 关闭;普通入驻由组件自己收起面板,避免 dialog 与状态不同步。
1322
+ event.preventDefault()
1323
+ if (this.onboardingLocked) return
1324
+ this.activePanel = null
1325
+ }
1326
+
1327
+ private readonly closeOnboardingFromBackdrop = (): void => {
1328
+ if (this.onboardingLocked) return
1329
+ this.activePanel = null
1331
1330
  }
1332
1331
 
1333
1332
  private readonly handleOnboardingSkipped = (): void => {
@@ -381,6 +381,11 @@ export class UniplatOrganizationMultiEmployeeSelectorElement extends LitElement
381
381
  .checkbox {
382
382
  border-radius: 5px;
383
383
  }
384
+ .selection-indicator svg {
385
+ width: 12px;
386
+ height: 12px;
387
+ stroke-width: 2.4;
388
+ }
384
389
  .selected .checkbox {
385
390
  border-color: var(--uniplat-employee-selector-primary);
386
391
  color: #fff;
@@ -1549,7 +1554,7 @@ function icon(
1549
1554
  return html`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18" /></svg>`
1550
1555
  }
1551
1556
  if (name === 'check') {
1552
- return html`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4 10-10" /></svg>`
1557
+ return html`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 12 4 4 8-8" /></svg>`
1553
1558
  }
1554
1559
  if (name === 'organization') {
1555
1560
  return html`<svg viewBox="0 0 24 24" aria-hidden="true">
@@ -8,7 +8,6 @@ import {
8
8
  export type OrganizationOnboardingStep =
9
9
  | 'unassigned'
10
10
  | 'method-selection'
11
- | 'skip-confirmation'
12
11
  | 'enterprise-form'
13
12
  | 'enterprise-success'
14
13
  | 'team-form'
@@ -21,6 +20,10 @@ export interface OrganizationOnboardingConfig {
21
20
  * 强制入驻:隐藏所有跳过入口,无归属用户只能创建组织或退出登录。
22
21
  */
23
22
  mandatory?: boolean
23
+ /**
24
+ * 当前用户已归属某个组织时才允许取消创建;无归属用户没有取消入口。
25
+ */
26
+ canCancel?: boolean
24
27
  }
25
28
 
26
29
  export interface OrganizationOnboardingEventMap {
@@ -243,6 +246,12 @@ export class InternalOrganizationOnboarding extends LitElement {
243
246
  background: var(--uniplat-onboarding-primary);
244
247
  }
245
248
 
249
+ .selected-mark svg {
250
+ width: 12px;
251
+ height: 12px;
252
+ stroke-width: 2.4;
253
+ }
254
+
246
255
  .primary,
247
256
  .secondary {
248
257
  display: inline-flex;
@@ -472,8 +481,8 @@ export class InternalOrganizationOnboarding extends LitElement {
472
481
  declare private createdOrganization: CreatedOnboardingOrganization | null
473
482
 
474
483
  private transport: OrganizationOnboardingTransport | null = null
475
- private skipReturnStep: SkippableStep = 'method-selection'
476
484
  private mandatory = false
485
+ private canCancel = false
477
486
 
478
487
  constructor() {
479
488
  super()
@@ -503,8 +512,12 @@ export class InternalOrganizationOnboarding extends LitElement {
503
512
  if (config.mandatory !== undefined && typeof config.mandatory !== 'boolean') {
504
513
  throw new TypeError('organization-onboarding mandatory must be a boolean')
505
514
  }
515
+ if (config.canCancel !== undefined && typeof config.canCancel !== 'boolean') {
516
+ throw new TypeError('organization-onboarding canCancel must be a boolean')
517
+ }
506
518
  this.transport = config.transport
507
519
  this.mandatory = config.mandatory ?? false
520
+ this.canCancel = config.canCancel ?? false
508
521
  // 强制入驻不展示带“暂不创建”的无归属提示,直接进入创建流程。
509
522
  this.reset(this.mandatory ? 'method-selection' : (config.initialStep ?? 'method-selection'))
510
523
  }
@@ -518,25 +531,25 @@ export class InternalOrganizationOnboarding extends LitElement {
518
531
  if (!this.transport) return nothing
519
532
  if (this.step === 'unassigned') return this.renderUnassigned()
520
533
  if (this.step === 'method-selection') return this.renderMethodSelection()
521
- if (this.step === 'skip-confirmation') return this.renderSkipConfirmation()
522
534
  if (this.step === 'enterprise-form') return this.renderEnterpriseForm()
523
535
  if (this.step === 'team-form') return this.renderTeamForm()
524
536
  return this.renderSuccess()
525
537
  }
526
538
 
527
539
  /**
528
- * 普通模式保留原型的“跳过”;强制入驻时创建流程首屏只提供“退出登录”,
529
- * 表单页提供“返回”回到创建方式选择,用户不会被困在某个分支里。
540
+ * 已归属组织的用户可以“取消”本次创建;无归属用户没有取消入口。强制入驻时首屏
541
+ * 提供“退出登录”,表单页提供“返回”回到创建方式选择,用户不会被困在某个分支里。
530
542
  */
531
543
  private renderHeadingAction(step: SkippableStep) {
532
544
  if (!this.mandatory) {
545
+ if (!this.canCancel) return nothing
533
546
  return html`<button
534
547
  class="skip"
535
- part="skip-action"
548
+ part="cancel-action"
536
549
  type="button"
537
- @click=${() => this.openSkip(step)}
550
+ @click=${() => this.cancelOnboarding(step)}
538
551
  >
539
- 跳过
552
+ 取消
540
553
  </button>`
541
554
  }
542
555
  if (step === 'method-selection') {
@@ -667,39 +680,6 @@ export class InternalOrganizationOnboarding extends LitElement {
667
680
  `
668
681
  }
669
682
 
670
- private renderSkipConfirmation() {
671
- return html`
672
- <section class="card" part="shell" aria-label="确认跳过入驻">
673
- <div class="confirmation" part="skip-confirmation">
674
- <div class="confirmation-icon">${icon('alert')}</div>
675
- <h2 part="title">确定暂不创建团队 / 企业?</h2>
676
- <p class="description" part="description">
677
- 跳过后你仍可浏览官网与服务介绍,但无法办理社保、薪酬、用工等业务。随时可在顶部导航“创建团队
678
- / 企业”补充。
679
- </p>
680
- <div class="confirmation-actions">
681
- <button
682
- class="secondary"
683
- part="secondary-action"
684
- type="button"
685
- @click=${this.confirmSkip}
686
- >
687
- 跳过,先逛逛
688
- </button>
689
- <button
690
- class="primary"
691
- part="primary-action"
692
- type="button"
693
- @click=${this.continueCreating}
694
- >
695
- 继续创建
696
- </button>
697
- </div>
698
- </div>
699
- </section>
700
- `
701
- }
702
-
703
683
  private renderEnterpriseForm() {
704
684
  const errorCount = [
705
685
  this.enterpriseErrors.organizationName,
@@ -984,24 +964,16 @@ export class InternalOrganizationOnboarding extends LitElement {
984
964
  this.step = this.choice === 'enterprise' ? 'enterprise-form' : 'team-form'
985
965
  }
986
966
 
987
- private openSkip(returnStep: SkippableStep) {
988
- if (this.mandatory) return
989
- this.skipReturnStep = returnStep
990
- this.step = 'skip-confirmation'
967
+ // 取消只对已归属用户开放,直接收起创建流程,不再走“确定暂不创建”的确认页。
968
+ private cancelOnboarding(step: SkippableStep) {
969
+ if (this.mandatory || !this.canCancel) return
970
+ this.dispatchPublicEvent('onboarding-skipped', { source: step })
991
971
  }
992
972
 
993
973
  private readonly requestLogout = () => {
994
974
  this.dispatchPublicEvent('logout-requested', {})
995
975
  }
996
976
 
997
- private readonly continueCreating = () => {
998
- this.step = this.skipReturnStep
999
- }
1000
-
1001
- private readonly confirmSkip = () => {
1002
- this.dispatchPublicEvent('onboarding-skipped', { source: this.skipReturnStep })
1003
- }
1004
-
1005
977
  private readonly skipUnassigned = () => {
1006
978
  this.dispatchPublicEvent('onboarding-skipped', { source: 'unassigned' })
1007
979
  }
@@ -1242,5 +1214,5 @@ function icon(name: IconName): TemplateResult {
1242
1214
  <path d="M12 3 2.8 20h18.4zM12 9v4M12 17h.01" />
1243
1215
  </svg>`
1244
1216
  }
1245
- return html`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4 10-10" /></svg>`
1217
+ return html`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 12 4 4 8-8" /></svg>`
1246
1218
  }
@@ -354,6 +354,12 @@ export class UniplatOrganizationSelector extends LitElement {
354
354
  background: var(--uniplat-organization-selector-primary);
355
355
  }
356
356
 
357
+ .organization.selected .indicator svg {
358
+ width: 12px;
359
+ height: 12px;
360
+ stroke-width: 2.4;
361
+ }
362
+
357
363
  .footer-row {
358
364
  display: flex;
359
365
  align-items: center;
@@ -926,5 +932,5 @@ function icon(name: OrganizationSelectorType | 'check' | 'chevron' | 'search'):
926
932
  <path d="M21 21l-6 -6" />
927
933
  </svg>`
928
934
  }
929
- return html`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4 10-10" /></svg>`
935
+ return html`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 12 4 4 8-8" /></svg>`
930
936
  }
@@ -240,9 +240,10 @@ describe('<uniplat-account-context>', () => {
240
240
  const onboardingRoot = onboarding?.shadowRoot
241
241
  expect(onboardingRoot?.textContent).toContain('创建你的团队 / 企业')
242
242
  expect(onboardingRoot?.textContent).not.toContain('跳过')
243
+ expect(onboardingRoot?.textContent).not.toContain('取消')
243
244
  expect(onboardingRoot?.textContent).not.toContain('暂不创建')
244
245
  expect(onboardingRoot?.textContent).not.toContain('先逛逛')
245
- expect(onboardingRoot?.querySelector('[part="skip-action"]')).toBeNull()
246
+ expect(onboardingRoot?.querySelector('[part="cancel-action"]')).toBeNull()
246
247
 
247
248
  const logoutAction = onboardingRoot?.querySelector<HTMLButtonElement>('[part="logout-action"]')
248
249
  expect(logoutAction?.textContent?.trim()).toBe('退出登录')
@@ -307,6 +308,36 @@ describe('<uniplat-account-context>', () => {
307
308
  expect(context.shadowRoot?.querySelector('[part="onboarding-panel"]')).toBeNull()
308
309
  })
309
310
 
311
+ it('centers the optional onboarding flow in a dialog that can still be dismissed', async () => {
312
+ const context = document.createElement(ACCOUNT_CONTEXT_TAG)
313
+ context.configure({
314
+ applicationName: APPLICATION_NAME,
315
+ transport: { load: async () => serverProjection() },
316
+ organizationOnboarding: { transport: createMockOrganizationOnboardingTransport() },
317
+ })
318
+ document.body.append(context)
319
+ await vi.waitFor(() => expect(context.shadowRoot?.textContent).toContain('企业 A'))
320
+
321
+ context.startOrganizationOnboarding()
322
+ await settle(context)
323
+
324
+ const modal = context.shadowRoot?.querySelector<HTMLDialogElement>('dialog.onboarding-modal')
325
+ expect(modal).not.toBeNull()
326
+ expect(modal?.hasAttribute('open')).toBe(true)
327
+ expect(context.shadowRoot?.querySelector('.floating-panel')).toBeNull()
328
+ const onboardingRoot = context.shadowRoot?.querySelector(
329
+ 'uniplat-internal-organization-onboarding',
330
+ )?.shadowRoot
331
+ const cancel = onboardingRoot?.querySelector('[part="cancel-action"]')
332
+ expect(cancel?.textContent?.trim()).toBe('取消')
333
+ expect(onboardingRoot?.textContent).not.toContain('跳过')
334
+
335
+ // 有归属用户点开创建流程时,遮罩点击可以关闭。
336
+ modal?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
337
+ await settle(context)
338
+ expect(context.shadowRoot?.querySelector('dialog.onboarding-modal')).toBeNull()
339
+ })
340
+
310
341
  it('exempts the organization invite page so the join flow is not covered', async () => {
311
342
  history.replaceState(null, '', '/org-invite?code=INVITE-CODE-1234')
312
343
  try {
@@ -165,7 +165,7 @@ describe('internal organization onboarding', () => {
165
165
  )
166
166
  })
167
167
 
168
- it('emits confirmed skip, join, authentication, upgrade and invitation actions', async () => {
168
+ it('emits cancel, join, authentication, upgrade and invitation actions', async () => {
169
169
  const onboarding = document.createElement(INTERNAL_ORGANIZATION_ONBOARDING_TAG)
170
170
  const skipped = vi.fn()
171
171
  const join = vi.fn()
@@ -175,11 +175,17 @@ describe('internal organization onboarding', () => {
175
175
  document.body.append(onboarding)
176
176
  await settle(onboarding)
177
177
 
178
- buttonByText(onboarding, '跳过').click()
178
+ // 无归属用户没有取消入口。
179
+ expect(onboarding.shadowRoot?.querySelector('[part="cancel-action"]')).toBeNull()
180
+ expect(onboarding.shadowRoot?.textContent).not.toContain('跳过')
181
+
182
+ onboarding.configure({ transport: createTransport(), canCancel: true })
179
183
  await settle(onboarding)
180
- expect(onboarding.shadowRoot?.textContent).toContain('确定暂不创建团队 / 企业')
181
- buttonByText(onboarding, '跳过,先逛逛').click()
184
+ const cancel = onboarding.shadowRoot?.querySelector<HTMLButtonElement>('[part="cancel-action"]')
185
+ expect(cancel?.textContent?.trim()).toBe('取消')
186
+ cancel?.click()
182
187
  expect(skipped.mock.calls[0]?.[0].detail).toEqual({ source: 'method-selection' })
188
+ expect(onboarding.shadowRoot?.textContent).not.toContain('确定暂不创建团队 / 企业')
183
189
 
184
190
  onboarding.start('unassigned')
185
191
  await settle(onboarding)
@@ -6,7 +6,7 @@
6
6
  >
7
7
  > 分支:`main`
8
8
  >
9
- > 交接基线提交:`d19ac72 feat(web-components): 无归属用户强制走创建组织流程`
9
+ > 交接基线提交:`2aadb87 feat(web-components): 组织邀请页豁免强制入驻`
10
10
 
11
11
  ## 1. Claude 接手后的第一步
12
12
 
@@ -23,9 +23,9 @@ git log -5 --oneline --decorate
23
23
  交接文档更新前的 Git 状态为:
24
24
 
25
25
  ```text
26
- main 领先 origin/main 9 个提交(未推送)
26
+ main origin/main 同步
27
27
  工作树无代码改动
28
- HEAD = d19ac72
28
+ HEAD = 2aadb87
29
29
  ```
30
30
 
31
31
  不要清理、覆盖或提交根目录已有的 `.tgz`。它们是历史及当前本地发布产物,已由 `.gitignore` 忽略。
@@ -38,9 +38,9 @@ HEAD = d19ac72
38
38
 
39
39
  | 位置 | npm 名称 | 当前本地版本 | 说明 |
40
40
  | ------------------------- | -------------------------------------- | -----------: | --------------------------------------- |
41
- | 根目录 | `@csntgao/uni-base` | `0.2.0` | 当前公开发布入口;npmjs 已发布此版本 |
41
+ | 根目录 | `@csntgao/uni-base` | `0.4.0` | 当前公开发布入口;npmjs 已发布此版本 |
42
42
  | `packages/core` | `@uniplat/ai-employee-client` | `0.14.0` | UI 无关的 HTTP/WebSocket/Core 状态机 |
43
- | `packages/web-components` | `@uniplat/ai-employee-web-components` | `0.25.0` | Lit Web Components 与独立浏览器 runtime |
43
+ | `packages/web-components` | `@uniplat/ai-employee-web-components` | `0.27.0` | Lit Web Components 与独立浏览器 runtime |
44
44
  | `examples/vanilla` | `@uniplat/ai-employee-vanilla-example` | 私有示例 | 必须消费 workspace 包的构建产物 |
45
45
 
46
46
  注意:根包改名为 `@csntgao/uni-base` 不代表两个 workspace 包、已有 Web Component 标签或公开类型可以随之改名。`@uniplat/*` 名称和 `uniplat-*` 标签仍是公开契约。
@@ -73,20 +73,47 @@ HEAD = d19ac72
73
73
 
74
74
  ### 4.-2 无归属用户强制入驻
75
75
 
76
- 最新提交:
76
+ 相关提交:
77
77
 
78
78
  ```text
79
79
  d19ac72 feat(web-components): 无归属用户强制走创建组织流程
80
+ acfeed4 feat(web-components)!: 强制入驻去掉跳过开关并恢复邀请码加入
81
+ efd56ef fix(web-components): 去掉强制入驻邀请码入口并隐藏无身份工作台
82
+ 2aadb87 feat(web-components): 组织邀请页豁免强制入驻
80
83
  ```
81
84
 
82
- 账户投影零归属时,`<uniplat-account-context>` 用原生 `dialog.showModal()`(top layer,不受 Host
83
- 导航 `transform/overflow` 影响)自动打开强制态创建流程:不渲染跳过、无归属提示、邀请码加入与
84
- 关闭按钮,遮罩点击、`Escape` `cancel` 都不关闭,只能创建组织或点“退出登录”派发
85
- `logout-requested`。企业/团队表单页的“跳过”换成“返回”,创建成功页的认证、升级或邀请动作才关闭
86
- 对话框。Host 可用 `organizationOnboarding.mandatory: false` 退回可跳过行为,默认开启。
87
-
88
- 该行为**覆盖**了 40.1 原型 4.1 的“暂不创建,先逛逛”和 S.0 图中“0 归属不弹窗”两条既有约定,
89
- 是按用户新需求做的行为变更,不是实现偏差。
85
+ 新运行机制:**个人用户不允许以非员工身份留在系统中**,跳过与“先逛逛”不再存在。当前实现:
86
+
87
+ 1. 账户投影零归属时,`<uniplat-account-context>` 用原生 `dialog.showModal()`(top layer,不受
88
+ Host 导航 `transform/overflow` 影响)自动打开强制态创建流程:不渲染跳过、无归属提示、邀请码
89
+ 入口与关闭按钮,遮罩点击、`Escape` `cancel` 都不关闭。
90
+ 2. 首屏出口只有创建企业、创建团队和“退出登录”(派发 `logout-requested`)。企业/团队表单页的
91
+ “跳过”换成“返回”,创建成功页的认证、升级或邀请动作才关闭对话框。
92
+ 3. **没有可跳过开关**:`organizationOnboarding` 只接收 `transport` 与可选 `invitePaths`,历史
93
+ `mandatory: false` 不再放行(有用例守着)。
94
+ 4. 没有任何企业员工身份时,账户胶囊仍渲染,但**不显示工作台入口**,即使配置了 `workbench`;
95
+ 有员工身份的组织或 Open Application `oid=0` 未选中组织时按既有规则显示。
96
+ 5. 组织邀请页豁免:当前 URL 末段路径命中 `invitePaths`(默认 `['org-invite']`)且 `code` 非空时
97
+ 不弹对话框,`pathname` 与 hash 路由都参与匹配。组件只取布尔值,邀请码不进状态、DOM、事件或
98
+ 日志;缺 `code` 或离开邀请页(`popstate`/`hashchange`)后仍无身份时立即重新强制。
99
+
100
+ 该行为**覆盖**了 40.1 原型 4.1 的“暂不创建,先逛逛”、4.2 的个人用户导航态和 S.0 图中
101
+ “0 归属不弹窗”几条既有约定,是按用户新规则做的行为变更,不是实现偏差。
102
+
103
+ 按新规则仍未清理完的矛盾(用户已确认审计结论,尚未授权逐项动手):
104
+
105
+ - `organization-onboarding.ts` 的 `unassigned`、`skip-confirmation` 步骤与 `onboarding-skipped`
106
+ 公开事件仍在,`start('unassigned')` 能把“先逛逛”拿回来;
107
+ - `account-context` 的个人态样式与 `user-menu` 的“暂无可切换的企业或团队”空态仍在;
108
+ - `user-menu` 在无当前组织时仍渲染“我的员工信息”,而 `account-context.openEmployeeInfo()` 会
109
+ 静默关掉菜单,是死入口;
110
+ - Host 未配置 `organizationOnboarding.transport` 或 `transport.load()` 失败时不弹对话框,
111
+ 零归属用户会被放行;
112
+ - `resolveOrganizationEntryDecision()` 仍返回 `{ kind: 'personal' }`,官网
113
+ `use-official-website-auth.ts` 已接该分支,改动需两边同步并出需求 v0_2。
114
+
115
+ 官网侧仍需实现 `/org-invite?code=...` 页面(校验邀请码、调用加入接口、成功后调用
116
+ `accountContext.refresh()`);SDK 只保证不挡住它。
90
117
 
91
118
  ### 4.-1 账户胶囊固定宽度
92
119
 
@@ -274,13 +301,13 @@ Account Context 对外只派发净化后的 `LOAD_FAILED`,精确排错应看
274
301
 
275
302
  ## 7. 当前构建和验证状态
276
303
 
277
- 在 `d19ac72` 上最近一次完整检查结果:
304
+ 在 `2aadb87` 上最近一次完整检查结果:
278
305
 
279
306
  ```text
280
307
  pnpm format:check 通过
281
308
  pnpm lint 通过
282
309
  pnpm typecheck 通过
283
- pnpm test 25 个测试文件通过;281 passed,2 skipped
310
+ pnpm test 25 个测试文件通过;287 passed,2 skipped
284
311
  pnpm build core、web-components、runtime、vanilla 全部通过
285
312
  ```
286
313
 
@@ -288,7 +315,7 @@ pnpm build core、web-components、runtime、vanilla 全部通过
288
315
 
289
316
  ```text
290
317
  /Users/gaostudio/uni-base/uniplat-ai-employee-client-0.14.0.tgz
291
- /Users/gaostudio/uni-base/uniplat-ai-employee-web-components-0.25.0.tgz
318
+ /Users/gaostudio/uni-base/uniplat-ai-employee-web-components-0.27.0.tgz
292
319
  ```
293
320
 
294
321
  根包本地产物:
@@ -321,9 +348,9 @@ pnpm --filter @uniplat/ai-employee-web-components pack
321
348
 
322
349
  ```text
323
350
  Manifest: https://10.10.10.131:8787/current.json
324
- Version: 0.25.0
325
- Module: https://10.10.10.131:8787/releases/0.25.0/index.js
326
- SHA-256: 3a37ff4225d346b9f32bdc1ee725a9050f5e0b631520f387407067bca6384631
351
+ Version: 0.27.0
352
+ Module: https://10.10.10.131:8787/releases/0.27.0/index.js
353
+ SHA-256: a5dfb56d11b60b0e3d475b24b2590751f830806e0907ed7013a38fdf05fae1f5
327
354
  ```
328
355
 
329
356
  最后验证结果:
@@ -331,7 +358,7 @@ SHA-256: 3a37ff4225d346b9f32bdc1ee725a9050f5e0b631520f387407067bca6384631
331
358
  - `current.json`:HTTP 200、`Cache-Control: no-store`;
332
359
  - 版本模块:HTTP 200、CORS `*`、`immutable`;
333
360
  - 远端模块 hash 与本地一致;
334
- - `current.json` 指向 `./releases/0.25.0/index.js`。
361
+ - `current.json` 指向 `./releases/0.27.0/index.js`。
335
362
 
336
363
  静态服务未启动时可执行:
337
364
 
@@ -353,12 +380,12 @@ https://registry.npmjs.org/
353
380
 
354
381
  ```text
355
382
  npm whoami csntgao(已登录)
356
- @csntgao/uni-base 0.2.0 已发布并回读校验
383
+ @csntgao/uni-base 0.4.0 已发布并回读校验
357
384
  @uniplat/ai-employee-client 未发布,PUT 返回 404(无 scope 创建权限)
358
385
  @uniplat/ai-employee-web-components 未发布,同上
359
386
  ```
360
387
 
361
- `@csntgao/uni-base@0.2.0` 的远端 shasum 为 `b02599ed1443de9909b7e005767ecc045e7a67db`,
388
+ `@csntgao/uni-base@0.4.0` 的远端 shasum 为 `51397b9e1f897641ea265ba4c74621c81d4a6fc4`,
362
389
  与本地打包一致。发布后远端 `npm view` 有几秒传播延迟,第一次回读到旧版本要重试再确认。注意该包没有 `main`/`exports`/`files`,发布内容是 91 个源码文件,
363
390
  不是可直接 `import` 的库;调用方仍应使用 runtime 或 workspace 包。
364
391