@csntgao/uni-base 0.1.4 → 0.2.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>`。点击账户菜单或个人态的“创建团队 / 企业”必须直接启动内部流程,不得向 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` 与 `cancel` 都不关闭,用户只能创建组织或点“退出登录”派发 `logout-requested`;企业/团队表单页提供“返回”回到创建方式选择,创建成功页的认证、升级或邀请动作才是关闭对话框的终点。Host 可用 `organizationOnboarding.mandatory: false` 退回原型 4.1 的可跳过行为。强制态必须使用 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
 
@@ -45,7 +45,7 @@ examples/vanilla 原生 HTML / JavaScript 接入示例
45
45
 
46
46
  `<uniplat-organization-single-employee-selector>` 是语义独立的组织员工单选公共组件。它可与多选组件复用内部数据控制器和真实 transport,但公开标签、`open()` 参数和确认 detail 必须独立。单选界面不渲染多选的已选右侧栏和移动端已选页签,候选区占满内容宽度,选择标识必须为单选圆点而非复选框。单选组件只接收 `initialSelectedEmployeeId`,确认时只返回 `{ employeeId, employee }`;不得向任一组件暴露 `selectionMode`,也不得让单选组件接收 `maxSelection` 或多选 ID 数组。候选数据继续只包含当前 JWT 组织内在职且启用的员工,JWT 和身份字段安全边界与多选组件一致。
47
47
 
48
- `<uniplat-account-context>` 是 40.1 原型导航右侧的统一账户工作台组件,不渲染品牌和全局菜单。它在一个 Host 元素内包含工作台入口、账户胶囊、应用入口浮层和 6.7 企业/团队切换浮层;两个浮层互斥并绝对定位,不改变导航布局。组件根据 Host 提供的 `applicationName` 和账户 transport 自行加载当前应用对应的组织投影与企业认证状态;用户资料复用 `list_available_organizations` 顶层的 `display_name`、`avatar_url` 与 `mobile_masked`,不得再请求 `profile_summary_v2`,Host 不再组装这些展示数据。它支持无归属个人态、企业未认证/审核中/未通过/已认证/认证过期和团队态;组织切换由 Host 响应事件,应用加载与 ticket 跳转复用独立的 Application Switcher transport。选中组织时,账户胶囊和菜单资料区必须使用服务端投影中该组织的员工姓名与头像;没有选中组织或该组织没有员工身份时,回退到用户资料。组织列表严格使用原型中的企业建筑与团队成员图标,不得替换成员工头像或组织首字。
48
+ `<uniplat-account-context>` 是 40.1 原型导航右侧的统一账户工作台组件,不渲染品牌和全局菜单。它在一个 Host 元素内包含工作台入口、账户胶囊、应用入口浮层和 6.7 企业/团队切换浮层;两个浮层互斥并绝对定位,不改变导航布局。组件根据 Host 提供的 `applicationName` 和账户 transport 自行加载当前应用对应的组织投影与企业认证状态;用户资料复用 `list_available_organizations` 顶层的 `display_name`、`avatar_url` 与 `mobile_masked`,不得再请求 `profile_summary_v2`,Host 不再组装这些展示数据。它支持无归属个人态、企业未认证/审核中/未通过/已认证/认证过期和团队态;组织切换由 Host 响应事件,应用加载与 ticket 跳转复用独立的 Application Switcher transport。选中组织时,账户胶囊和菜单资料区必须使用服务端投影中该组织的员工姓名与头像;没有选中组织或该组织没有员工身份时,回退到用户资料。账户胶囊宽度固定,胶囊内姓名超过 6 个字、组织名称超过 8 个字必须截断为省略号,完整值只放在 `title` 上,不得让长名称撑宽胶囊挤压导航左侧内容;展开菜单内仍显示完整名称。组织列表严格使用原型中的企业建筑与团队成员图标,不得替换成员工头像或组织首字。
49
49
 
50
50
  `<uniplat-notification-center>` 是内嵌通知中心面板,复刻 AE-Enterprise-Admin 通知中心原型的两栏收件箱:面板顶栏、五类页签、列表、详情、关联资源、附件下载与“全部标已读”。**组件和 core 都不建立 WebSocket 连接**:Host 保留自己唯一的 `/api/user-realtime/ws` 连接并订阅 `notification` channel,把事件原始负载通过 `applyRealtimeEvent()` 喂给组件;`authenticate` 首帧、心跳、游标、重连和断连提示全部归 Host。组件只负责收件箱投影、增量更新和未读数,顶栏铃铛仍属 Host chrome。不得为通知新增第二条 socket 或平行实时通道。
51
51
 
@@ -324,10 +324,12 @@ userMenu.configure({
324
324
  ```ts
325
325
  accountContext.configure({
326
326
  applicationName,
327
- organizationOnboarding: { transport },
327
+ organizationOnboarding: { transport, mandatory },
328
328
  })
329
329
  ```
330
330
 
331
+ `mandatory` 默认 `true`,非布尔值一律拒绝。
332
+
331
333
  transport 只提供 `createEnterprise()` 和 `createTeam()`;稳定冲突错误使用 `OrganizationOnboardingTransportError`,不得把原始响应、JWT 或内部异常透传给组件。`createEnterprise()` 接收组织名称、企业名称与统一社会信用代码,`createTeam()` 只接收团队名称。创建成功响应只包含稳定 ID、展示名称和 `enterprise | team` 类型。
332
334
 
333
335
  真实 UniplatBase 对接使用 `createUniplatBaseOrganizationOnboardingTransport({ serviceBaseUrl, getAccessToken })`,复用 `system.org/create_organization`。每次创建前必须重新取 JWT,并只放入 `Authorization: Bearer` header;请求不得携带调用方身份字段。只对白名单冲突码做字段级映射,`INVALID_ARGUMENT`、`ORGANIZATION_CREATE_FAILED` 和所有非协议异常统一收敛为 `UNKNOWN`。
@@ -435,11 +437,11 @@ npm 包名、导出类型名、Custom Element 标签、`configure()` 结构、
435
437
  - Account Context 内嵌工作台需测试不注册独立应用切换标签、JWT 只交给 transport、HTTP Authorization header、入口净化、当前组织级应用加 `application_scope=open` 应用的筛选、缺字段时 `oid=0` 兼容、`issue_ticket` 空请求体、签发响应来源进入新 Fragment、公共事件无 JWT/Ticket、Ticket 只进入局部导航 URL、目标页面先清 Fragment 再按初始化应用登录、响应应用绑定校验、加载错误、仅请求应用列表和应用选择交互。
436
438
  - User Login 需测试组件注册与导出、三个固定页签、不可用能力禁用、原型中不存在图形验证码且不调用兼容方法、逐字段失焦校验和协议错误态、稳定后端错误码映射、短信倒计时、密码登录、扫码待确认/已扫码/过期/成功状态、注册成功过渡页、2 秒自动入驻和立即开始、页签切换与断开后的定时器清理、登录回调,以及 JWT、密码和二维码 challenge 不进入 URL、DOM、公共事件、日志或安全错误。
437
439
  - User Menu 需测试组件注册与导出、40.1 6.7 原型关键内容、个人账号与当前组织员工信息入口、企业认证与团队样式、安全头像与文字回退、Host 受控选中状态、重新配置、空组织状态、完整手机号拒绝、创建与退出事件、普通文本插值,以及公共事件只包含必要的组织 ID,不包含显示名、手机号、组织名称、头像 URL、JWT 或内部配置。
438
- - 内置组织入驻需测试所有原型步骤、逐字段校验、稳定 transport 错误映射、企业/团队成功状态、跳过与未展开流程事件、普通文本插值,以及公共事件不包含企业信息、信用代码、JWT 或原始异常。真实 UniplatBase transport 还必须测试每次请求重新取 JWT、JWT 只进入 Authorization header、企业/团队请求字段、成功响应净化和未知错误收敛。
440
+ - 内置组织入驻需测试所有原型步骤、逐字段校验、稳定 transport 错误映射、企业/团队成功状态、跳过与未展开流程事件、普通文本插值,以及公共事件不包含企业信息、信用代码、JWT 或原始异常。强制入驻还需测试零归属投影自动打开 top layer 对话框、不渲染跳过与无归属提示、`Escape` 与外部点击都不关闭、“退出登录”只派发空 detail、表单页“返回”回到创建方式选择、成功页动作才关闭对话框,以及 `mandatory: false` 保持原可跳过行为。真实 UniplatBase transport 还必须测试每次请求重新取 JWT、JWT 只进入 Authorization header、企业/团队请求字段、成功响应净化和未知错误收敛。
439
441
  - Organization Selector 需测试完整列表、企业认证与团队类型样式、脱敏账户摘要与完整手机号拒绝、按名称和统一社会信用代码搜索且代码不进入 DOM、类型筛选与常显计数、“最近进入 / 全部组织”分组与默认选中、带目标名称的主按钮、无匹配与空归属状态、记住偏好、创建/进入/退出登录事件、`modal` 强制态无关闭按钮且遮罩点击与 `Escape` 都不关闭并在移除后解除 `Escape` 拦截、非法 `presentation` 与重复 ID 拒绝和普通文本插值;事件只包含稳定组织 ID 与偏好,不含姓名、手机号、组织名称或信用代码。`resolveOrganizationEntryDecision()` 需测试 0 个归属、单一归属、命中与失效的记住选择、2 个及以上的默认选中项和非法输入拒绝。
440
442
  - Organization Multi Employee Selector 需测试部门树、员工分页和搜索、初始选择补取、跨部门按员工 ID 去重、多选上限、受保护项、确认/取消事件、独立滚动、移动视图、错误重试、身份切换清理,以及 JWT、完整手机号、身份字段和后端错误不进入 DOM、事件、URL或日志。专用 transport 必须测试每次请求重新读取 JWT、只使用 Authorization header、组织级 directory scope 和无调用方身份参数。
441
443
  - Organization Single Employee Selector 需测试独立标签与导出、初始单个 ID 回显、新选择替换旧选择、未选时禁止确认、受保护项、标量确认 detail、拒绝多选打开参数,以及与多选组件相同的数据和认证安全边界。
442
- - Account Context 需测试必填应用名、组件通过 transport 自行加载用户资料与完整应用组织投影、无归属、企业五种认证状态和团队状态、当前组织员工身份优先及无组织用户身份回退、安全头像与加载失败回退、工作台、账户菜单与内置入驻浮层互斥、浮层不改变导航布局、内嵌应用九宫格、创建入口直接启动内置入驻、Host 响应组织切换事件、退出和公共事件安全。专用 UniplatBase transport 还必须测试不请求 `profile_summary_v2`、从 `list_available_organizations` 顶层读取用户资料、按 `application_name` 加载投影、Open Application 的 `organization_oid` 切换、组织级应用目标 `xid` 选择、JWT 只进入 Authorization header 和 Host `setAccessToken()`、响应净化、稳定错误以及公共数据无 JWT;组件范围不得扩展为品牌或完整导航。
444
+ - Account Context 需测试必填应用名、组件通过 transport 自行加载用户资料与完整应用组织投影、无归属、企业五种认证状态和团队状态、当前组织员工身份优先及无组织用户身份回退、安全头像与加载失败回退、胶囊内姓名 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
445
  - 通知中心需测试组件注册与导出、两栏首版输出、数据驱动分类页签(`includeTypeFacets` 随收件箱请求、后端 `type_facets` 优先、无 facet 回退累积、筛选后页签不塌缩、展示名取 `notification_type_name` 且为空时回退原始 code、契约四个码不得残留内置中文名、实时新分类入目录)、未读角标与 `unread-count-change`、打开未读即标记已读、全部已读、加载更多、关联资源事件只含安全业务标识、附件经 transport 下载、Markdown 净化、`applyRealtimeEvent()` 不建立任何 WebSocket 且原地更新列表与未读数、事件无法映射时降级刷新、收件箱在途时延后 resync 不丢事件、`configure()` 之前与移除之后喂入事件无副作用、错误横幅为净化文案,以及 JWT 与后端原始正文不进入 DOM、公共事件或日志。小型通知浮层还需测试初始关闭、4 条未读投影、未读总数、严格原型结构与 SVG 图标、单条/全部已读、完整通知页意图事件只含稳定 ID、外部关闭、空态以及 Host 实时事件更新。
444
446
  - 使用 fake timer 的测试必须恢复 timer;构造 fake WebSocket 时不要把测试凭据写入 URL。
445
447
  - 修复竞态或生命周期问题时增加回归测试,不只验证最终 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>`。点击账户菜单或个人态的“创建团队 / 企业”必须直接启动内部流程,不得向 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` 与 `cancel` 都不关闭,用户只能创建组织或点“退出登录”派发 `logout-requested`;企业/团队表单页提供“返回”回到创建方式选择,创建成功页的认证、升级或邀请动作才是关闭对话框的终点。Host 可用 `organizationOnboarding.mandatory: false` 退回原型 4.1 的可跳过行为。强制态必须使用 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
 
@@ -45,7 +45,7 @@ examples/vanilla 原生 HTML / JavaScript 接入示例
45
45
 
46
46
  `<uniplat-organization-single-employee-selector>` 是语义独立的组织员工单选公共组件。它可与多选组件复用内部数据控制器和真实 transport,但公开标签、`open()` 参数和确认 detail 必须独立。单选界面不渲染多选的已选右侧栏和移动端已选页签,候选区占满内容宽度,选择标识必须为单选圆点而非复选框。单选组件只接收 `initialSelectedEmployeeId`,确认时只返回 `{ employeeId, employee }`;不得向任一组件暴露 `selectionMode`,也不得让单选组件接收 `maxSelection` 或多选 ID 数组。候选数据继续只包含当前 JWT 组织内在职且启用的员工,JWT 和身份字段安全边界与多选组件一致。
47
47
 
48
- `<uniplat-account-context>` 是 40.1 原型导航右侧的统一账户工作台组件,不渲染品牌和全局菜单。它在一个 Host 元素内包含工作台入口、账户胶囊、应用入口浮层和 6.7 企业/团队切换浮层;两个浮层互斥并绝对定位,不改变导航布局。组件根据 Host 提供的 `applicationName` 和账户 transport 自行加载当前应用对应的组织投影与企业认证状态;用户资料复用 `list_available_organizations` 顶层的 `display_name`、`avatar_url` 与 `mobile_masked`,不得再请求 `profile_summary_v2`,Host 不再组装这些展示数据。它支持无归属个人态、企业未认证/审核中/未通过/已认证/认证过期和团队态;组织切换由 Host 响应事件,应用加载与 ticket 跳转复用独立的 Application Switcher transport。选中组织时,账户胶囊和菜单资料区必须使用服务端投影中该组织的员工姓名与头像;没有选中组织或该组织没有员工身份时,回退到用户资料。组织列表严格使用原型中的企业建筑与团队成员图标,不得替换成员工头像或组织首字。
48
+ `<uniplat-account-context>` 是 40.1 原型导航右侧的统一账户工作台组件,不渲染品牌和全局菜单。它在一个 Host 元素内包含工作台入口、账户胶囊、应用入口浮层和 6.7 企业/团队切换浮层;两个浮层互斥并绝对定位,不改变导航布局。组件根据 Host 提供的 `applicationName` 和账户 transport 自行加载当前应用对应的组织投影与企业认证状态;用户资料复用 `list_available_organizations` 顶层的 `display_name`、`avatar_url` 与 `mobile_masked`,不得再请求 `profile_summary_v2`,Host 不再组装这些展示数据。它支持无归属个人态、企业未认证/审核中/未通过/已认证/认证过期和团队态;组织切换由 Host 响应事件,应用加载与 ticket 跳转复用独立的 Application Switcher transport。选中组织时,账户胶囊和菜单资料区必须使用服务端投影中该组织的员工姓名与头像;没有选中组织或该组织没有员工身份时,回退到用户资料。账户胶囊宽度固定,胶囊内姓名超过 6 个字、组织名称超过 8 个字必须截断为省略号,完整值只放在 `title` 上,不得让长名称撑宽胶囊挤压导航左侧内容;展开菜单内仍显示完整名称。组织列表严格使用原型中的企业建筑与团队成员图标,不得替换成员工头像或组织首字。
49
49
 
50
50
  `<uniplat-notification-center>` 是内嵌通知中心面板,复刻 AE-Enterprise-Admin 通知中心原型的两栏收件箱:面板顶栏、五类页签、列表、详情、关联资源、附件下载与“全部标已读”。**组件和 core 都不建立 WebSocket 连接**:Host 保留自己唯一的 `/api/user-realtime/ws` 连接并订阅 `notification` channel,把事件原始负载通过 `applyRealtimeEvent()` 喂给组件;`authenticate` 首帧、心跳、游标、重连和断连提示全部归 Host。组件只负责收件箱投影、增量更新和未读数,顶栏铃铛仍属 Host chrome。不得为通知新增第二条 socket 或平行实时通道。
51
51
 
@@ -324,10 +324,12 @@ userMenu.configure({
324
324
  ```ts
325
325
  accountContext.configure({
326
326
  applicationName,
327
- organizationOnboarding: { transport },
327
+ organizationOnboarding: { transport, mandatory },
328
328
  })
329
329
  ```
330
330
 
331
+ `mandatory` 默认 `true`,非布尔值一律拒绝。
332
+
331
333
  transport 只提供 `createEnterprise()` 和 `createTeam()`;稳定冲突错误使用 `OrganizationOnboardingTransportError`,不得把原始响应、JWT 或内部异常透传给组件。`createEnterprise()` 接收组织名称、企业名称与统一社会信用代码,`createTeam()` 只接收团队名称。创建成功响应只包含稳定 ID、展示名称和 `enterprise | team` 类型。
332
334
 
333
335
  真实 UniplatBase 对接使用 `createUniplatBaseOrganizationOnboardingTransport({ serviceBaseUrl, getAccessToken })`,复用 `system.org/create_organization`。每次创建前必须重新取 JWT,并只放入 `Authorization: Bearer` header;请求不得携带调用方身份字段。只对白名单冲突码做字段级映射,`INVALID_ARGUMENT`、`ORGANIZATION_CREATE_FAILED` 和所有非协议异常统一收敛为 `UNKNOWN`。
@@ -435,11 +437,11 @@ npm 包名、导出类型名、Custom Element 标签、`configure()` 结构、
435
437
  - Account Context 内嵌工作台需测试不注册独立应用切换标签、JWT 只交给 transport、HTTP Authorization header、入口净化、当前组织级应用加 `application_scope=open` 应用的筛选、缺字段时 `oid=0` 兼容、`issue_ticket` 空请求体、签发响应来源进入新 Fragment、公共事件无 JWT/Ticket、Ticket 只进入局部导航 URL、目标页面先清 Fragment 再按初始化应用登录、响应应用绑定校验、加载错误、仅请求应用列表和应用选择交互。
436
438
  - User Login 需测试组件注册与导出、三个固定页签、不可用能力禁用、原型中不存在图形验证码且不调用兼容方法、逐字段失焦校验和协议错误态、稳定后端错误码映射、短信倒计时、密码登录、扫码待确认/已扫码/过期/成功状态、注册成功过渡页、2 秒自动入驻和立即开始、页签切换与断开后的定时器清理、登录回调,以及 JWT、密码和二维码 challenge 不进入 URL、DOM、公共事件、日志或安全错误。
437
439
  - User Menu 需测试组件注册与导出、40.1 6.7 原型关键内容、个人账号与当前组织员工信息入口、企业认证与团队样式、安全头像与文字回退、Host 受控选中状态、重新配置、空组织状态、完整手机号拒绝、创建与退出事件、普通文本插值,以及公共事件只包含必要的组织 ID,不包含显示名、手机号、组织名称、头像 URL、JWT 或内部配置。
438
- - 内置组织入驻需测试所有原型步骤、逐字段校验、稳定 transport 错误映射、企业/团队成功状态、跳过与未展开流程事件、普通文本插值,以及公共事件不包含企业信息、信用代码、JWT 或原始异常。真实 UniplatBase transport 还必须测试每次请求重新取 JWT、JWT 只进入 Authorization header、企业/团队请求字段、成功响应净化和未知错误收敛。
440
+ - 内置组织入驻需测试所有原型步骤、逐字段校验、稳定 transport 错误映射、企业/团队成功状态、跳过与未展开流程事件、普通文本插值,以及公共事件不包含企业信息、信用代码、JWT 或原始异常。强制入驻还需测试零归属投影自动打开 top layer 对话框、不渲染跳过与无归属提示、`Escape` 与外部点击都不关闭、“退出登录”只派发空 detail、表单页“返回”回到创建方式选择、成功页动作才关闭对话框,以及 `mandatory: false` 保持原可跳过行为。真实 UniplatBase transport 还必须测试每次请求重新取 JWT、JWT 只进入 Authorization header、企业/团队请求字段、成功响应净化和未知错误收敛。
439
441
  - Organization Selector 需测试完整列表、企业认证与团队类型样式、脱敏账户摘要与完整手机号拒绝、按名称和统一社会信用代码搜索且代码不进入 DOM、类型筛选与常显计数、“最近进入 / 全部组织”分组与默认选中、带目标名称的主按钮、无匹配与空归属状态、记住偏好、创建/进入/退出登录事件、`modal` 强制态无关闭按钮且遮罩点击与 `Escape` 都不关闭并在移除后解除 `Escape` 拦截、非法 `presentation` 与重复 ID 拒绝和普通文本插值;事件只包含稳定组织 ID 与偏好,不含姓名、手机号、组织名称或信用代码。`resolveOrganizationEntryDecision()` 需测试 0 个归属、单一归属、命中与失效的记住选择、2 个及以上的默认选中项和非法输入拒绝。
440
442
  - Organization Multi Employee Selector 需测试部门树、员工分页和搜索、初始选择补取、跨部门按员工 ID 去重、多选上限、受保护项、确认/取消事件、独立滚动、移动视图、错误重试、身份切换清理,以及 JWT、完整手机号、身份字段和后端错误不进入 DOM、事件、URL或日志。专用 transport 必须测试每次请求重新读取 JWT、只使用 Authorization header、组织级 directory scope 和无调用方身份参数。
441
443
  - Organization Single Employee Selector 需测试独立标签与导出、初始单个 ID 回显、新选择替换旧选择、未选时禁止确认、受保护项、标量确认 detail、拒绝多选打开参数,以及与多选组件相同的数据和认证安全边界。
442
- - Account Context 需测试必填应用名、组件通过 transport 自行加载用户资料与完整应用组织投影、无归属、企业五种认证状态和团队状态、当前组织员工身份优先及无组织用户身份回退、安全头像与加载失败回退、工作台、账户菜单与内置入驻浮层互斥、浮层不改变导航布局、内嵌应用九宫格、创建入口直接启动内置入驻、Host 响应组织切换事件、退出和公共事件安全。专用 UniplatBase transport 还必须测试不请求 `profile_summary_v2`、从 `list_available_organizations` 顶层读取用户资料、按 `application_name` 加载投影、Open Application 的 `organization_oid` 切换、组织级应用目标 `xid` 选择、JWT 只进入 Authorization header 和 Host `setAccessToken()`、响应净化、稳定错误以及公共数据无 JWT;组件范围不得扩展为品牌或完整导航。
444
+ - Account Context 需测试必填应用名、组件通过 transport 自行加载用户资料与完整应用组织投影、无归属、企业五种认证状态和团队状态、当前组织员工身份优先及无组织用户身份回退、安全头像与加载失败回退、胶囊内姓名 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
445
  - 通知中心需测试组件注册与导出、两栏首版输出、数据驱动分类页签(`includeTypeFacets` 随收件箱请求、后端 `type_facets` 优先、无 facet 回退累积、筛选后页签不塌缩、展示名取 `notification_type_name` 且为空时回退原始 code、契约四个码不得残留内置中文名、实时新分类入目录)、未读角标与 `unread-count-change`、打开未读即标记已读、全部已读、加载更多、关联资源事件只含安全业务标识、附件经 transport 下载、Markdown 净化、`applyRealtimeEvent()` 不建立任何 WebSocket 且原地更新列表与未读数、事件无法映射时降级刷新、收件箱在途时延后 resync 不丢事件、`configure()` 之前与移除之后喂入事件无副作用、错误横幅为净化文案,以及 JWT 与后端原始正文不进入 DOM、公共事件或日志。小型通知浮层还需测试初始关闭、4 条未读投影、未读总数、严格原型结构与 SVG 图标、单条/全部已读、完整通知页意图事件只含稳定 ID、外部关闭、空态以及 Host 实时事件更新。
444
446
  - 使用 fake timer 的测试必须恢复 timer;构造 fake WebSocket 时不要把测试凭据写入 URL。
445
447
  - 修复竞态或生命周期问题时增加回归测试,不只验证最终 DOM。
package/README.md CHANGED
@@ -298,7 +298,22 @@ 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
- 内部流程会经账户组件派发 `onboarding-skipped`、`join-organization-requested`、`organization-created`、`enterprise-authentication-requested`、`enterprise-authentication-deferred`、`team-upgrade-requested`、`team-invite-members-requested` 和安全的 `error` 事件。Host 可通过账户组件的 `onboarding-panel` part 定制该浮层。
301
+ 内部流程会经账户组件派发 `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
+
303
+ 无归属用户默认必须先入驻。账户投影返回零归属时,组件用原生 `dialog.showModal()` 自动打开强制态创建流程:
304
+
305
+ ```js
306
+ accountContext.configure({
307
+ applicationName: 'OfficialWebsite',
308
+ transport: accountTransport,
309
+ organizationOnboarding: {
310
+ transport: organizationOnboardingTransport,
311
+ mandatory: true, // 默认值;传 false 回到可跳过的原型 4.1 行为
312
+ },
313
+ })
314
+ ```
315
+
316
+ 强制态下不渲染“跳过”“暂不创建,先逛逛”“用邀请码加入”和关闭按钮,遮罩点击、`Escape` 与原生 `cancel` 都不关闭,用户只有两条出路:创建组织,或点“退出登录”(派发 `logout-requested`,由 Host 清理登录态)。企业/团队表单页提供“返回”回到创建方式选择,避免困在错误分支;创建成功页的“去认证 / 稍后认证 / 升级为企业 / 邀请成员”是关闭对话框的终点。对话框使用 top layer,不受 Host 导航 `transform` 或 `overflow` 影响。
302
317
 
303
318
  ### Organization Selector
304
319
 
@@ -421,6 +436,8 @@ selector.addEventListener('selection-confirmed', (event) => {
421
436
 
422
437
  账户区和组织菜单中的工作台、选择器、认证状态、选中、创建及退出图标使用 40.1 原型指定的 Tabler Icons 2.47.0 原始 SVG 路径;组织列表左侧使用原型中的企业建筑或团队成员图标。组件将这些原型资源打入独立运行时,不依赖 Host 的图标字体或全局样式。
423
438
 
439
+ 账户胶囊宽度固定:胶囊内姓名超过 6 个字、当前组织名称超过 8 个字会截断为省略号,完整值只放在 `title` 上,长名称不会撑宽胶囊挤压导航左侧内容;展开后的菜单仍显示完整名称。
440
+
424
441
  ```js
425
442
  import {
426
443
  createUniplatBaseAccountContextTransport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@csntgao/uni-base",
3
- "version": "0.1.4",
3
+ "version": "0.2.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.24.0",
3
+ "version": "0.25.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,
@@ -17,6 +17,10 @@ import type { UniplatUserMenu, UserMenuConfig } from './user-menu'
17
17
 
18
18
  const INTERNAL_APPLICATION_SWITCHER_TAG = 'uniplat-internal-application-switcher'
19
19
 
20
+ // 账户胶囊固定宽度,超出的姓名与组织名在胶囊内截断,完整值只放在 title 里。
21
+ const CAPSULE_DISPLAY_NAME_MAX_CHARS = 6
22
+ const CAPSULE_ORGANIZATION_NAME_MAX_CHARS = 8
23
+
20
24
  if (
21
25
  typeof customElements !== 'undefined' &&
22
26
  !customElements.get(INTERNAL_APPLICATION_SWITCHER_TAG)
@@ -113,6 +117,11 @@ export interface AccountContextConfig {
113
117
  */
114
118
  organizationOnboarding?: {
115
119
  transport: OrganizationOnboardingTransport
120
+ /**
121
+ * 无归属用户是否必须先入驻。默认开启:加载到零归属投影时组件自动以
122
+ * 强制模式对话框打开创建流程,用户只能创建组织或退出登录。
123
+ */
124
+ mandatory?: boolean
116
125
  }
117
126
  }
118
127
 
@@ -168,6 +177,7 @@ export class UniplatAccountContext extends LitElement {
168
177
  viewModel: { state: true },
169
178
  activePanel: { state: true },
170
179
  employeeInfo: { state: true },
180
+ mandatoryOnboarding: { state: true },
171
181
  }
172
182
 
173
183
  static override styles = css`
@@ -328,16 +338,19 @@ export class UniplatAccountContext extends LitElement {
328
338
  display: none;
329
339
  }
330
340
 
341
+ /* 胶囊宽度固定,姓名与组织名再长也不挤压导航左侧内容。 */
331
342
  .copy {
332
343
  display: flex;
344
+ width: 96px;
333
345
  min-width: 0;
346
+ flex: none;
334
347
  flex-direction: column;
335
348
  line-height: 1.3;
336
349
  }
337
350
 
338
351
  .display-name {
339
352
  overflow: hidden;
340
- max-width: 120px;
353
+ max-width: 100%;
341
354
  font-size: 12.5px;
342
355
  font-weight: 700;
343
356
  text-overflow: ellipsis;
@@ -348,7 +361,7 @@ export class UniplatAccountContext extends LitElement {
348
361
  display: inline-flex;
349
362
  align-items: center;
350
363
  gap: 4px;
351
- max-width: 212px;
364
+ max-width: 100%;
352
365
  margin-top: 1px;
353
366
  }
354
367
 
@@ -443,6 +456,35 @@ export class UniplatAccountContext extends LitElement {
443
456
  width: min(400px, calc(100vw - 24px));
444
457
  }
445
458
 
459
+ /*
460
+ * 强制入驻使用原生 dialog 的 top layer,不受 Host 导航 transform / overflow 影响;
461
+ * 无关闭按钮,遮罩点击与 Escape 都不关闭。
462
+ */
463
+ .onboarding-modal {
464
+ position: fixed;
465
+ z-index: 1200;
466
+ inset: 0;
467
+ display: grid;
468
+ width: 100vw;
469
+ max-width: none;
470
+ height: 100dvh;
471
+ max-height: none;
472
+ overflow: auto;
473
+ margin: 0;
474
+ padding: 24px;
475
+ border: 0;
476
+ background: transparent;
477
+ place-items: center;
478
+ }
479
+
480
+ .onboarding-modal::backdrop {
481
+ background: rgb(20 35 56 / 56%);
482
+ }
483
+
484
+ .onboarding-modal uniplat-internal-organization-onboarding {
485
+ width: min(400px, 100%);
486
+ }
487
+
446
488
  .employee-info-modal {
447
489
  position: fixed;
448
490
  z-index: 1100;
@@ -673,9 +715,12 @@ export class UniplatAccountContext extends LitElement {
673
715
  declare private viewModel: ResolvedAccountContext | null
674
716
  declare private activePanel: ActivePanel
675
717
  declare private employeeInfo: AccountContextEmployeeInfo | null
718
+ declare private mandatoryOnboarding: boolean
676
719
 
677
720
  private workbenchConfig: ApplicationSwitcherConfig | null = null
678
721
  private organizationOnboardingTransport: OrganizationOnboardingTransport | null = null
722
+ private organizationOnboardingMandatory = true
723
+ private configuredOnboardingMandatory = false
679
724
  private applicationName = ''
680
725
  private accountTransport: AccountContextTransport | null = null
681
726
  private showWorkbench = true
@@ -692,6 +737,7 @@ export class UniplatAccountContext extends LitElement {
692
737
  this.viewModel = null
693
738
  this.activePanel = null
694
739
  this.employeeInfo = null
740
+ this.mandatoryOnboarding = false
695
741
  }
696
742
 
697
743
  configure(config: AccountContextConfig): void {
@@ -704,6 +750,10 @@ export class UniplatAccountContext extends LitElement {
704
750
  this.organizationOnboardingTransport = normalizeOrganizationOnboardingTransport(
705
751
  config.organizationOnboarding?.transport,
706
752
  )
753
+ this.organizationOnboardingMandatory = normalizeMandatoryOnboarding(
754
+ config.organizationOnboarding?.mandatory,
755
+ )
756
+ this.mandatoryOnboarding = false
707
757
  this.configuredApplicationSwitcher = null
708
758
  this.configuredUserMenu = null
709
759
  this.configuredOrganizationOnboarding = null
@@ -830,6 +880,29 @@ export class UniplatAccountContext extends LitElement {
830
880
  showWorkbench: config.showWorkbench ?? true,
831
881
  showVerificationStatus: organization ? (config.showVerificationStatus ?? true) : false,
832
882
  }
883
+ this.syncMandatoryOnboarding()
884
+ }
885
+
886
+ /**
887
+ * 无归属用户必须先入驻:加载到零归属投影时自动以强制模式对话框打开创建流程。
888
+ * 创建成功后投影不再为空,但对话框保持强制态直到用户在成功页完成后续动作,
889
+ * 避免界面从模式对话框跳回小浮层。
890
+ */
891
+ private syncMandatoryOnboarding(): void {
892
+ if (
893
+ this.organizationOnboardingMandatory &&
894
+ this.organizationOnboardingTransport &&
895
+ this.viewModel &&
896
+ this.viewModel.organizations.length === 0 &&
897
+ !this.viewModel.organization
898
+ ) {
899
+ this.mandatoryOnboarding = true
900
+ this.activePanel = 'onboarding'
901
+ }
902
+ }
903
+
904
+ private get onboardingLocked(): boolean {
905
+ return this.mandatoryOnboarding && this.activePanel === 'onboarding'
833
906
  }
834
907
 
835
908
  override connectedCallback(): void {
@@ -912,7 +985,12 @@ export class UniplatAccountContext extends LitElement {
912
985
  }
913
986
  </span>
914
987
  <span class="copy">
915
- <span class="display-name" part="display-name">${viewModel.displayName}</span>
988
+ <span
989
+ class="display-name"
990
+ part="display-name"
991
+ title=${capsuleTitle(viewModel.displayName, CAPSULE_DISPLAY_NAME_MAX_CHARS)}
992
+ >${truncateCapsuleText(viewModel.displayName, CAPSULE_DISPLAY_NAME_MAX_CHARS)}</span
993
+ >
916
994
  ${
917
995
  organization
918
996
  ? this.renderOrganizationLine(organization, viewModel.showVerificationStatus)
@@ -964,14 +1042,26 @@ export class UniplatAccountContext extends LitElement {
964
1042
  const onboarding = this.renderRoot.querySelector<InternalOrganizationOnboarding>(
965
1043
  INTERNAL_ORGANIZATION_ONBOARDING_TAG,
966
1044
  )
967
- if (onboarding && onboarding !== this.configuredOrganizationOnboarding) {
1045
+ if (
1046
+ onboarding &&
1047
+ (onboarding !== this.configuredOrganizationOnboarding ||
1048
+ this.configuredOnboardingMandatory !== this.mandatoryOnboarding)
1049
+ ) {
968
1050
  const transport = this.organizationOnboardingTransport
969
1051
  if (transport) {
970
- onboarding.configure({ transport, initialStep: 'method-selection' })
1052
+ onboarding.configure({
1053
+ transport,
1054
+ initialStep: 'method-selection',
1055
+ mandatory: this.mandatoryOnboarding,
1056
+ })
971
1057
  this.configuredOrganizationOnboarding = onboarding
1058
+ this.configuredOnboardingMandatory = this.mandatoryOnboarding
972
1059
  }
973
1060
  }
974
- if (!onboarding) this.configuredOrganizationOnboarding = null
1061
+ if (!onboarding) {
1062
+ this.configuredOrganizationOnboarding = null
1063
+ this.configuredOnboardingMandatory = false
1064
+ }
975
1065
 
976
1066
  const employeeInfoModal = this.renderRoot.querySelector<HTMLDialogElement>(
977
1067
  'dialog.employee-info-modal',
@@ -983,6 +1073,16 @@ export class UniplatAccountContext extends LitElement {
983
1073
  employeeInfoModal.setAttribute('open', '')
984
1074
  }
985
1075
  }
1076
+
1077
+ const onboardingModal =
1078
+ this.renderRoot.querySelector<HTMLDialogElement>('dialog.onboarding-modal')
1079
+ if (onboardingModal && !onboardingModal.open) {
1080
+ try {
1081
+ onboardingModal.showModal()
1082
+ } catch {
1083
+ onboardingModal.setAttribute('open', '')
1084
+ }
1085
+ }
986
1086
  }
987
1087
 
988
1088
  private renderOrganizationLine(
@@ -995,7 +1095,8 @@ export class UniplatAccountContext extends LitElement {
995
1095
  <span
996
1096
  class=${`organization-name${status === 'verified' || status === 'team' ? ' normal' : ''}`}
997
1097
  part="organization-name"
998
- >${organization.name}</span
1098
+ title=${capsuleTitle(organization.name, CAPSULE_ORGANIZATION_NAME_MAX_CHARS)}
1099
+ >${truncateCapsuleText(organization.name, CAPSULE_ORGANIZATION_NAME_MAX_CHARS)}</span
999
1100
  >
1000
1101
  ${
1001
1102
  status
@@ -1024,13 +1125,30 @@ export class UniplatAccountContext extends LitElement {
1024
1125
  </div>`
1025
1126
  }
1026
1127
  if (this.activePanel === 'onboarding' && this.organizationOnboardingTransport) {
1128
+ const onboarding = html`<uniplat-internal-organization-onboarding></uniplat-internal-organization-onboarding>`
1129
+ if (this.mandatoryOnboarding) {
1130
+ return html`<dialog
1131
+ class="onboarding-modal"
1132
+ part="onboarding-panel"
1133
+ aria-label="创建团队或企业"
1134
+ @cancel=${this.blockMandatoryOnboardingCancel}
1135
+ @onboarding-skipped=${this.handleOnboardingSkipped}
1136
+ @organization-created=${this.refreshAfterOrganizationCreated}
1137
+ @enterprise-authentication-requested=${this.finishMandatoryOnboarding}
1138
+ @enterprise-authentication-deferred=${this.finishMandatoryOnboarding}
1139
+ @team-upgrade-requested=${this.finishMandatoryOnboarding}
1140
+ @team-invite-members-requested=${this.finishMandatoryOnboarding}
1141
+ >
1142
+ ${onboarding}
1143
+ </dialog>`
1144
+ }
1027
1145
  return html`<div
1028
1146
  class="floating-panel onboarding-panel"
1029
1147
  part="onboarding-panel"
1030
- @onboarding-skipped=${this.closePanelAfterAction}
1148
+ @onboarding-skipped=${this.handleOnboardingSkipped}
1031
1149
  @organization-created=${this.refreshAfterOrganizationCreated}
1032
1150
  >
1033
- <uniplat-internal-organization-onboarding></uniplat-internal-organization-onboarding>
1151
+ ${onboarding}
1034
1152
  </div>`
1035
1153
  }
1036
1154
  if (this.activePanel === 'account') {
@@ -1181,6 +1299,24 @@ export class UniplatAccountContext extends LitElement {
1181
1299
  this.activePanel = null
1182
1300
  }
1183
1301
 
1302
+ private readonly blockMandatoryOnboardingCancel = (event: Event): void => {
1303
+ // 强制入驻不允许 Escape 关闭原生对话框。
1304
+ if (this.onboardingLocked) event.preventDefault()
1305
+ }
1306
+
1307
+ private readonly handleOnboardingSkipped = (): void => {
1308
+ // 强制入驻没有跳过入口;即使宿主自行派发也不放行。
1309
+ if (this.onboardingLocked) return
1310
+ this.activePanel = null
1311
+ }
1312
+
1313
+ // 成功页的认证 / 升级 / 邀请动作是强制入驻的终点,此时才允许关闭对话框。
1314
+ private readonly finishMandatoryOnboarding = (): void => {
1315
+ if (!this.mandatoryOnboarding) return
1316
+ this.mandatoryOnboarding = false
1317
+ this.activePanel = null
1318
+ }
1319
+
1184
1320
  private readonly openEmployeeInfo = (): void => {
1185
1321
  if (!this.viewModel?.organization) {
1186
1322
  this.activePanel = null
@@ -1255,11 +1391,17 @@ export class UniplatAccountContext extends LitElement {
1255
1391
  }
1256
1392
 
1257
1393
  private readonly handleDocumentPointerDown = (event: PointerEvent): void => {
1394
+ if (this.onboardingLocked) return
1258
1395
  if (this.activePanel && !event.composedPath().includes(this)) this.activePanel = null
1259
1396
  }
1260
1397
 
1261
1398
  private readonly handleDocumentKeyDown = (event: KeyboardEvent): void => {
1262
1399
  if (event.key !== 'Escape' || !this.activePanel) return
1400
+ if (this.onboardingLocked) {
1401
+ event.preventDefault()
1402
+ event.stopPropagation()
1403
+ return
1404
+ }
1263
1405
  const activePanel = this.activePanel
1264
1406
  this.activePanel = null
1265
1407
  const selector = activePanel === 'workbench' ? '.workbench' : '.account'
@@ -1320,6 +1462,14 @@ function normalizeWorkbench(
1320
1462
  return workbench
1321
1463
  }
1322
1464
 
1465
+ function normalizeMandatoryOnboarding(value: unknown): boolean {
1466
+ if (value === undefined) return true
1467
+ if (typeof value !== 'boolean') {
1468
+ throw new TypeError('account-context organizationOnboarding.mandatory must be a boolean')
1469
+ }
1470
+ return value
1471
+ }
1472
+
1323
1473
  function normalizeOrganizationOnboardingTransport(
1324
1474
  transport: OrganizationOnboardingTransport | undefined,
1325
1475
  ): OrganizationOnboardingTransport | null {
@@ -1474,6 +1624,15 @@ function normalizeOptionalText(value: unknown, maxLength: number): string {
1474
1624
  return normalized
1475
1625
  }
1476
1626
 
1627
+ function truncateCapsuleText(value: string, maxChars: number): string {
1628
+ const characters = [...value]
1629
+ return characters.length > maxChars ? `${characters.slice(0, maxChars).join('')}…` : value
1630
+ }
1631
+
1632
+ function capsuleTitle(value: string, maxChars: number): string | typeof nothing {
1633
+ return [...value].length > maxChars ? value : nothing
1634
+ }
1635
+
1477
1636
  function organizationStatus(
1478
1637
  organization: ResolvedAccountContextOrganization,
1479
1638
  ): EnterpriseVerificationStatus | 'team' {
@@ -17,12 +17,17 @@ export type OrganizationOnboardingStep =
17
17
  export interface OrganizationOnboardingConfig {
18
18
  transport: OrganizationOnboardingTransport
19
19
  initialStep?: 'unassigned' | 'method-selection'
20
+ /**
21
+ * 强制入驻:隐藏所有跳过入口,无归属用户只能创建组织或退出登录。
22
+ */
23
+ mandatory?: boolean
20
24
  }
21
25
 
22
26
  export interface OrganizationOnboardingEventMap {
23
27
  'onboarding-skipped': CustomEvent<{
24
28
  source: 'unassigned' | 'method-selection' | 'enterprise-form' | 'team-form'
25
29
  }>
30
+ 'logout-requested': CustomEvent<Record<string, never>>
26
31
  'join-organization-requested': CustomEvent<{ enterpriseName?: string }>
27
32
  'organization-created': CustomEvent<{ organizationId: string; type: 'enterprise' | 'team' }>
28
33
  'enterprise-authentication-requested': CustomEvent<{ organizationId: string }>
@@ -468,6 +473,7 @@ export class InternalOrganizationOnboarding extends LitElement {
468
473
 
469
474
  private transport: OrganizationOnboardingTransport | null = null
470
475
  private skipReturnStep: SkippableStep = 'method-selection'
476
+ private mandatory = false
471
477
 
472
478
  constructor() {
473
479
  super()
@@ -494,13 +500,18 @@ export class InternalOrganizationOnboarding extends LitElement {
494
500
  if (config.initialStep && !['unassigned', 'method-selection'].includes(config.initialStep)) {
495
501
  throw new TypeError('organization-onboarding initialStep is invalid')
496
502
  }
503
+ if (config.mandatory !== undefined && typeof config.mandatory !== 'boolean') {
504
+ throw new TypeError('organization-onboarding mandatory must be a boolean')
505
+ }
497
506
  this.transport = config.transport
498
- this.reset(config.initialStep ?? 'method-selection')
507
+ this.mandatory = config.mandatory ?? false
508
+ // 强制入驻不展示带“暂不创建”的无归属提示,直接进入创建流程。
509
+ this.reset(this.mandatory ? 'method-selection' : (config.initialStep ?? 'method-selection'))
499
510
  }
500
511
 
501
512
  start(step: 'unassigned' | 'method-selection' = 'method-selection'): void {
502
513
  if (!this.transport) throw new Error('organization-onboarding is not configured')
503
- this.reset(step)
514
+ this.reset(this.mandatory ? 'method-selection' : step)
504
515
  }
505
516
 
506
517
  override render() {
@@ -513,6 +524,41 @@ export class InternalOrganizationOnboarding extends LitElement {
513
524
  return this.renderSuccess()
514
525
  }
515
526
 
527
+ /**
528
+ * 普通模式保留原型的“跳过”;强制入驻时创建流程首屏只提供“退出登录”,
529
+ * 表单页提供“返回”回到创建方式选择,用户不会被困在某个分支里。
530
+ */
531
+ private renderHeadingAction(step: SkippableStep) {
532
+ if (!this.mandatory) {
533
+ return html`<button
534
+ class="skip"
535
+ part="skip-action"
536
+ type="button"
537
+ @click=${() => this.openSkip(step)}
538
+ >
539
+ 跳过
540
+ </button>`
541
+ }
542
+ if (step === 'method-selection') {
543
+ return html`<button
544
+ class="skip"
545
+ part="logout-action"
546
+ type="button"
547
+ @click=${this.requestLogout}
548
+ >
549
+ 退出登录
550
+ </button>`
551
+ }
552
+ return html`<button
553
+ class="skip"
554
+ part="back-action"
555
+ type="button"
556
+ @click=${this.openMethodSelection}
557
+ >
558
+ 返回
559
+ </button>`
560
+ }
561
+
516
562
  private renderUnassigned() {
517
563
  return html`
518
564
  <section class="card" part="shell" aria-label="无归属团队或企业">
@@ -565,14 +611,7 @@ export class InternalOrganizationOnboarding extends LitElement {
565
611
  办理社保、薪酬、用工等业务需先归属一个团队 / 企业
566
612
  </p>
567
613
  </div>
568
- <button
569
- class="skip"
570
- part="skip-action"
571
- type="button"
572
- @click=${() => this.openSkip('method-selection')}
573
- >
574
- 跳过
575
- </button>
614
+ ${this.renderHeadingAction('method-selection')}
576
615
  </div>
577
616
  <div class="options" role="list" part="method-options">
578
617
  ${this.renderOption(
@@ -672,14 +711,7 @@ export class InternalOrganizationOnboarding extends LitElement {
672
711
  <div class="content">
673
712
  <div class="heading-row">
674
713
  <div><h2 part="title">填写企业信息</h2></div>
675
- <button
676
- class="skip"
677
- part="skip-action"
678
- type="button"
679
- @click=${() => this.openSkip('enterprise-form')}
680
- >
681
- 跳过
682
- </button>
714
+ ${this.renderHeadingAction('enterprise-form')}
683
715
  </div>
684
716
  ${
685
717
  errorCount > 0
@@ -813,14 +845,7 @@ export class InternalOrganizationOnboarding extends LitElement {
813
845
  <div class="content">
814
846
  <div class="heading-row">
815
847
  <div><h2 part="title">填写团队信息</h2></div>
816
- <button
817
- class="skip"
818
- part="skip-action"
819
- type="button"
820
- @click=${() => this.openSkip('team-form')}
821
- >
822
- 跳过
823
- </button>
848
+ ${this.renderHeadingAction('team-form')}
824
849
  </div>
825
850
  <div class="team-notice" part="team-notice">
826
851
  ${icon('check')}<span
@@ -960,10 +985,15 @@ export class InternalOrganizationOnboarding extends LitElement {
960
985
  }
961
986
 
962
987
  private openSkip(returnStep: SkippableStep) {
988
+ if (this.mandatory) return
963
989
  this.skipReturnStep = returnStep
964
990
  this.step = 'skip-confirmation'
965
991
  }
966
992
 
993
+ private readonly requestLogout = () => {
994
+ this.dispatchPublicEvent('logout-requested', {})
995
+ }
996
+
967
997
  private readonly continueCreating = () => {
968
998
  this.step = this.skipReturnStep
969
999
  }
@@ -59,6 +59,36 @@ async function settle(context: UniplatAccountContext): Promise<void> {
59
59
  await Promise.resolve()
60
60
  }
61
61
 
62
+ function emptyProjection(): AccountContextProjection {
63
+ return {
64
+ applicationName: APPLICATION_NAME,
65
+ user: { displayName: '131****2222', maskedMobile: '131****2222', avatarText: '1' },
66
+ currentOrganizationId: null,
67
+ currentOrganization: null,
68
+ organizations: [],
69
+ }
70
+ }
71
+
72
+ async function mountUnassigned(
73
+ mandatory?: boolean,
74
+ ): Promise<{ context: UniplatAccountContext; logout: ReturnType<typeof vi.fn> }> {
75
+ const context = document.createElement(ACCOUNT_CONTEXT_TAG)
76
+ const logout = vi.fn()
77
+ context.addEventListener('logout-requested', logout)
78
+ context.configure({
79
+ applicationName: APPLICATION_NAME,
80
+ transport: { load: async () => emptyProjection() },
81
+ organizationOnboarding: {
82
+ transport: createMockOrganizationOnboardingTransport(),
83
+ ...(mandatory === undefined ? {} : { mandatory }),
84
+ },
85
+ })
86
+ document.body.append(context)
87
+ await vi.waitFor(() => expect(context.shadowRoot?.querySelector('.display-name')).not.toBeNull())
88
+ await settle(context)
89
+ return { context, logout }
90
+ }
91
+
62
92
  describe('<uniplat-account-context>', () => {
63
93
  afterEach(() => {
64
94
  document.body.replaceChildren()
@@ -193,6 +223,93 @@ describe('<uniplat-account-context>', () => {
193
223
  expect(menu?.shadowRoot?.textContent).toContain('新创建团队')
194
224
  })
195
225
 
226
+ it('forces unassigned users into a mandatory onboarding modal with only create or logout', async () => {
227
+ const { context, logout } = await mountUnassigned()
228
+
229
+ const modal = context.shadowRoot?.querySelector<HTMLDialogElement>('dialog.onboarding-modal')
230
+ expect(modal).not.toBeNull()
231
+ expect(modal?.getAttribute('aria-label')).toBe('创建团队或企业')
232
+ expect(modal?.hasAttribute('open')).toBe(true)
233
+ expect(context.shadowRoot?.querySelector('.floating-panel')).toBeNull()
234
+
235
+ const onboarding = context.shadowRoot?.querySelector('uniplat-internal-organization-onboarding')
236
+ const onboardingRoot = onboarding?.shadowRoot
237
+ expect(onboardingRoot?.textContent).toContain('创建你的团队 / 企业')
238
+ expect(onboardingRoot?.textContent).not.toContain('跳过')
239
+ expect(onboardingRoot?.textContent).not.toContain('暂不创建')
240
+ expect(onboardingRoot?.querySelector('[part="skip-action"]')).toBeNull()
241
+
242
+ const logoutAction = onboardingRoot?.querySelector<HTMLButtonElement>('[part="logout-action"]')
243
+ expect(logoutAction?.textContent?.trim()).toBe('退出登录')
244
+ logoutAction?.click()
245
+ expect(logout).toHaveBeenCalledTimes(1)
246
+ expect(logout.mock.calls[0]?.[0].detail).toEqual({})
247
+ })
248
+
249
+ it('keeps the mandatory onboarding modal open on outside clicks and Escape', async () => {
250
+ const { context } = await mountUnassigned()
251
+
252
+ document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
253
+ await settle(context)
254
+ expect(context.shadowRoot?.querySelector('.onboarding-modal')).not.toBeNull()
255
+
256
+ const escape = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })
257
+ document.dispatchEvent(escape)
258
+ await settle(context)
259
+ expect(escape.defaultPrevented).toBe(true)
260
+ expect(context.shadowRoot?.querySelector('.onboarding-modal')).not.toBeNull()
261
+ })
262
+
263
+ it('offers a way back to the creation choice instead of skipping inside the forced flow', async () => {
264
+ const { context } = await mountUnassigned()
265
+ const onboarding = context.shadowRoot?.querySelector('uniplat-internal-organization-onboarding')
266
+ const onboardingRoot = onboarding?.shadowRoot
267
+
268
+ onboardingRoot?.querySelector<HTMLButtonElement>('[part="primary-action"]')?.click()
269
+ await onboarding?.updateComplete
270
+ expect(onboardingRoot?.textContent).toContain('填写企业信息')
271
+ const back = onboardingRoot?.querySelector<HTMLButtonElement>('[part="back-action"]')
272
+ expect(back?.textContent?.trim()).toBe('返回')
273
+ expect(onboardingRoot?.querySelector('[part="skip-action"]')).toBeNull()
274
+
275
+ back?.click()
276
+ await onboarding?.updateComplete
277
+ expect(onboardingRoot?.textContent).toContain('创建你的团队 / 企业')
278
+ })
279
+
280
+ it('closes the mandatory modal only after the created organization success action', async () => {
281
+ const { context } = await mountUnassigned()
282
+ const onboarding = context.shadowRoot?.querySelector('uniplat-internal-organization-onboarding')
283
+ const onboardingRoot = onboarding?.shadowRoot
284
+
285
+ onboardingRoot?.querySelector<HTMLButtonElement>('[part="primary-action"]')?.click()
286
+ await onboarding?.updateComplete
287
+ const inputs = [...(onboardingRoot?.querySelectorAll<HTMLInputElement>('[part="input"]') ?? [])]
288
+ const values = ['星河科技', '北京星河科技有限公司', '91110108MA01XY7788']
289
+ inputs.forEach((input, index) => {
290
+ input.value = values[index] ?? ''
291
+ input.dispatchEvent(new Event('input', { bubbles: true }))
292
+ })
293
+ await onboarding?.updateComplete
294
+ onboardingRoot?.querySelector('form')?.dispatchEvent(new Event('submit', { bubbles: true }))
295
+
296
+ await vi.waitFor(() => expect(onboardingRoot?.textContent).toContain('已创建'))
297
+ expect(context.shadowRoot?.querySelector('.onboarding-modal')).not.toBeNull()
298
+
299
+ onboardingRoot?.querySelector<HTMLButtonElement>('[part="secondary-action"]')?.click()
300
+ await settle(context)
301
+ expect(context.shadowRoot?.querySelector('.onboarding-modal')).toBeNull()
302
+ expect(context.shadowRoot?.querySelector('[part="onboarding-panel"]')).toBeNull()
303
+ })
304
+
305
+ it('keeps the previous optional onboarding behaviour when mandatory is disabled', async () => {
306
+ const { context } = await mountUnassigned(false)
307
+
308
+ expect(context.shadowRoot?.querySelector('.onboarding-modal')).toBeNull()
309
+ expect(context.shadowRoot?.querySelector('[part="onboarding-panel"]')).toBeNull()
310
+ expect(context.shadowRoot?.textContent).toContain('创建团队 / 企业')
311
+ })
312
+
196
313
  it('registers and renders the personal account navigation state', async () => {
197
314
  expect(customElements.get(ACCOUNT_CONTEXT_TAG)).toBe(UniplatAccountContext)
198
315
  const context = document.createElement(ACCOUNT_CONTEXT_TAG)
@@ -273,7 +390,9 @@ describe('<uniplat-account-context>', () => {
273
390
  await settle(context)
274
391
 
275
392
  expect(context.shadowRoot?.textContent).toContain('工作台')
276
- expect(context.shadowRoot?.textContent).toContain('北京星河科技有限公司')
393
+ const capsuleOrganization = context.shadowRoot?.querySelector('.organization-name')
394
+ expect(capsuleOrganization?.textContent?.trim()).toBe('北京星河科技有限…')
395
+ expect(capsuleOrganization?.getAttribute('title')).toBe('北京星河科技有限公司')
277
396
  const statusIcon = context.shadowRoot?.querySelector(`.status-icon.${cssClass}`)
278
397
  expect(statusIcon?.getAttribute('aria-label')).toBe(label)
279
398
  expect(statusIcon?.querySelector('svg')?.dataset.prototypeIcon).toBe(iconName)
@@ -797,6 +916,49 @@ describe('<uniplat-account-context>', () => {
797
916
  expect(image?.hidden).toBe(false)
798
917
  })
799
918
 
919
+ it('truncates long capsule names so the navigation width stays fixed', async () => {
920
+ const context = document.createElement(ACCOUNT_CONTEXT_TAG)
921
+ context.configure({
922
+ applicationName: APPLICATION_NAME,
923
+ displayName: '欧阳夏尔泽辉安',
924
+ organization: {
925
+ id: 'enterprise-huaqin',
926
+ name: '北京华勤百业技术服务有限公司',
927
+ type: 'enterprise',
928
+ applicationNames: [APPLICATION_NAME],
929
+ verificationStatus: 'verified',
930
+ },
931
+ })
932
+ document.body.append(context)
933
+ await settle(context)
934
+
935
+ const capsuleName = context.shadowRoot?.querySelector('.display-name')
936
+ const capsuleOrganization = context.shadowRoot?.querySelector('.organization-name')
937
+ expect(capsuleName?.textContent?.trim()).toBe('欧阳夏尔泽辉…')
938
+ expect(capsuleName?.getAttribute('title')).toBe('欧阳夏尔泽辉安')
939
+ expect(capsuleOrganization?.textContent?.trim()).toBe('北京华勤百业技术…')
940
+ expect(capsuleOrganization?.getAttribute('title')).toBe('北京华勤百业技术服务有限公司')
941
+
942
+ context.configure({
943
+ applicationName: APPLICATION_NAME,
944
+ displayName: '高高高',
945
+ organization: {
946
+ id: 'team-small',
947
+ name: '小团队',
948
+ type: 'team',
949
+ applicationNames: [APPLICATION_NAME],
950
+ },
951
+ })
952
+ await settle(context)
953
+
954
+ const shortName = context.shadowRoot?.querySelector('.display-name')
955
+ const shortOrganization = context.shadowRoot?.querySelector('.organization-name')
956
+ expect(shortName?.textContent?.trim()).toBe('高高高')
957
+ expect(shortName?.hasAttribute('title')).toBe(false)
958
+ expect(shortOrganization?.textContent?.trim()).toBe('小团队')
959
+ expect(shortOrganization?.hasAttribute('title')).toBe(false)
960
+ })
961
+
800
962
  it('requires enterprise status, rejects raw mobile numbers and renders labels as text', async () => {
801
963
  const context = document.createElement(ACCOUNT_CONTEXT_TAG)
802
964
  expect(() =>
@@ -826,6 +988,8 @@ describe('<uniplat-account-context>', () => {
826
988
  document.body.append(context)
827
989
  await settle(context)
828
990
  expect(context.shadowRoot?.querySelector('script')).toBeNull()
829
- expect(context.shadowRoot?.textContent).toContain('<script>unsafe()</script>')
991
+ const capsuleName = context.shadowRoot?.querySelector('.display-name')
992
+ expect(capsuleName?.textContent?.trim()).toBe('<scrip…')
993
+ expect(capsuleName?.getAttribute('title')).toBe('<script>unsafe()</script>')
830
994
  })
831
995
  })
@@ -6,7 +6,7 @@
6
6
  >
7
7
  > 分支:`main`
8
8
  >
9
- > 交接基线提交:`7d2f9bb docs(docs): 修正失效的原型与接口说明文档路径`
9
+ > 交接基线提交:`605c783 fix(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 4 个提交(未推送)
26
+ main 领先 origin/main 7 个提交(未推送)
27
27
  工作树无代码改动
28
- HEAD = 7d2f9bb
28
+ HEAD = 605c783
29
29
  ```
30
30
 
31
31
  不要清理、覆盖或提交根目录已有的 `.tgz`。它们是历史及当前本地发布产物,已由 `.gitignore` 忽略。
@@ -38,9 +38,9 @@ HEAD = 7d2f9bb
38
38
 
39
39
  | 位置 | npm 名称 | 当前本地版本 | 说明 |
40
40
  | ------------------------- | -------------------------------------- | -----------: | --------------------------------------- |
41
- | 根目录 | `@csntgao/uni-base` | `0.1.4` | 当前公开发布入口;npmjs 尚未发布此版本 |
41
+ | 根目录 | `@csntgao/uni-base` | `0.1.5` | 当前公开发布入口;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.24.0` | Lit Web Components 与独立浏览器 runtime |
43
+ | `packages/web-components` | `@uniplat/ai-employee-web-components` | `0.24.1` | 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-*` 标签仍是公开契约。
@@ -71,6 +71,19 @@ HEAD = 7d2f9bb
71
71
 
72
72
  ## 4. 近期完成的改动
73
73
 
74
+ ### 4.-1 账户胶囊固定宽度
75
+
76
+ 最新提交:
77
+
78
+ ```text
79
+ 605c783 fix(web-components): 固定账户胶囊宽度并截断过长姓名与组织名
80
+ ```
81
+
82
+ 导航右侧账户胶囊的 `.copy` 固定 96px;胶囊内姓名超过 6 个字、当前组织名称超过 8 个字按
83
+ 字符数截断为 `…`,完整值只写入 `title`,未超长时不加 `title`。展开后的 User Menu 仍显示
84
+ 完整名称,截断只作用于胶囊。相关常量在 `packages/web-components/src/account-context.ts`
85
+ 的 `CAPSULE_DISPLAY_NAME_MAX_CHARS` 与 `CAPSULE_ORGANIZATION_NAME_MAX_CHARS`。
86
+
74
87
  ### 4.0 多归属组织选择改为 S.0 / S.1
75
88
 
76
89
  最新提交:
@@ -244,13 +257,13 @@ Account Context 对外只派发净化后的 `LOAD_FAILED`,精确排错应看
244
257
 
245
258
  ## 7. 当前构建和验证状态
246
259
 
247
- 在 `67d8d83` 上最近一次完整检查结果:
260
+ 在 `605c783` 上最近一次完整检查结果:
248
261
 
249
262
  ```text
250
263
  pnpm format:check 通过
251
264
  pnpm lint 通过
252
265
  pnpm typecheck 通过
253
- pnpm test 25 个测试文件通过;275 passed,2 skipped
266
+ pnpm test 25 个测试文件通过;276 passed,2 skipped
254
267
  pnpm build core、web-components、runtime、vanilla 全部通过
255
268
  ```
256
269
 
@@ -258,7 +271,7 @@ pnpm build core、web-components、runtime、vanilla 全部通过
258
271
 
259
272
  ```text
260
273
  /Users/gaostudio/uni-base/uniplat-ai-employee-client-0.14.0.tgz
261
- /Users/gaostudio/uni-base/uniplat-ai-employee-web-components-0.24.0.tgz
274
+ /Users/gaostudio/uni-base/uniplat-ai-employee-web-components-0.24.1.tgz
262
275
  ```
263
276
 
264
277
  根包本地产物:
@@ -291,9 +304,9 @@ pnpm --filter @uniplat/ai-employee-web-components pack
291
304
 
292
305
  ```text
293
306
  Manifest: https://10.10.10.131:8787/current.json
294
- Version: 0.24.0
295
- Module: https://10.10.10.131:8787/releases/0.24.0/index.js
296
- SHA-256: 2cc1ba9ab48eb20e23cd1f95e68ca9d70413c602aa5f400828147020d4cc0341
307
+ Version: 0.24.1
308
+ Module: https://10.10.10.131:8787/releases/0.24.1/index.js
309
+ SHA-256: c45894bb393dd5a99e484f01ce302cf39024a4d6c1124cf8f33ab9db6063405d
297
310
  ```
298
311
 
299
312
  最后验证结果:
@@ -301,7 +314,7 @@ SHA-256: 2cc1ba9ab48eb20e23cd1f95e68ca9d70413c602aa5f400828147020d4cc0341
301
314
  - `current.json`:HTTP 200、`Cache-Control: no-store`;
302
315
  - 版本模块:HTTP 200、CORS `*`、`immutable`;
303
316
  - 远端模块 hash 与本地一致;
304
- - `current.json` 指向 `./releases/0.24.0/index.js`。
317
+ - `current.json` 指向 `./releases/0.24.1/index.js`。
305
318
 
306
319
  静态服务未启动时可执行:
307
320
 
@@ -322,27 +335,31 @@ https://registry.npmjs.org/
322
335
  当前远端状态:
323
336
 
324
337
  ```text
325
- @csntgao/uni-base 最新公开版本:0.1.3
326
- 本地待发布版本:0.1.4
327
- npm whoami:E401 Unauthorized
338
+ npm whoami csntgao(已登录)
339
+ @csntgao/uni-base 0.1.5 已发布并回读校验
340
+ @uniplat/ai-employee-client 未发布,PUT 返回 404(无 scope 创建权限)
341
+ @uniplat/ai-employee-web-components 未发布,同上
328
342
  ```
329
343
 
330
- 因此 `@csntgao/uni-base@0.1.4` 尚未发布,不能声称 npm 发布成功。用户完成以下登录后再继续:
344
+ `@csntgao/uni-base@0.1.5` 的远端 shasum 为 `b962d7140a2ae62247b4ed3a45782bb821238b17`,
345
+ 与本地打包一致。发布后远端 `npm view` 有几秒传播延迟,第一次回读到旧版本要重试再确认。注意该包没有 `main`/`exports`/`files`,发布内容是 91 个源码文件,
346
+ 不是可直接 `import` 的库;调用方仍应使用 runtime 或 workspace 包。
331
347
 
332
- ```bash
333
- npm login --registry=https://registry.npmjs.org/
334
- npm whoami
335
- ```
336
-
337
- 登录成功后必须重新查询远端版本;确认 `0.1.4` 不存在,再从已提交且干净的工作树发布并回读:
348
+ 发布需要浏览器 2FA。本机 shell 不是交互终端,直接 `npm publish` 会以 `EOTP` 退出;
349
+ 用伪终端包一层即可让 npm 停下来等浏览器授权:
338
350
 
339
351
  ```bash
340
- npm view @csntgao/uni-base version
341
- npm publish --access public
342
- npm view @csntgao/uni-base@0.1.4 version
352
+ script -q /dev/null npm publish <绝对路径或 tarball> --access public
343
353
  ```
344
354
 
345
- 两个 `@uniplat/*` 包当前在 npmjs 不存在或对当前账号不可见。发布它们需要 `@uniplat` 组织管理员把账号 `csntgao` 加入组织,并授予创建、发布 public package 的权限;不要通过重命名内部包规避公开契约。
355
+ 注意两个陷阱:`npm publish packages/core` 会被当成 git 仓库简写;`npm publish <目录>`
356
+ 不会改写 `workspace:*`,发布 web-components 必须使用 `pnpm pack` 生成的 tarball。
357
+
358
+ 两个 `@uniplat/*` 包仍未发布,属外部阻塞。`npm access list packages @uniplat` 只显示
359
+ `@uniplat/html5-qrcode: read-write`,且该包 maintainer 是 `ytl003
360
+ <yanghua19970726@163.com>`,`npm org ls uniplat` 无输出,说明当前账号在该 scope 下
361
+ 只有单包权限、没有创建新包的权限。需要 scope 所有者发布,或把 `@uniplat` 作为组织把
362
+ `csntgao` 加入并授予创建 public package 的权限;不要通过重命名内部包规避公开契约。
346
363
 
347
364
  ## 9. 发布流程
348
365
 
@@ -388,7 +405,7 @@ Enterprise Admin:/Users/gaostudio/ae_design/实现/ae-enterprise-admin
388
405
  1. 完整阅读 `AGENTS.md`;
389
406
  2. 确认 Git 分支、工作树和最新提交;
390
407
  3. 根据用户当前任务定位对应组件及测试,不要顺手改公开契约;
391
- 4. 若继续 npm 发布,先让用户恢复 npmjs 登录,再核对远端版本;
408
+ 4. 若继续 npm 发布,两个 `@uniplat/*` 包仍缺 scope 创建权限,见第 8.2 节;
392
409
  5. 若继续官网登录联调,重点确认 `setAccessToken()` 完成后是否调用 Account Context `refresh()`;
393
410
  6. 所有认证问题排查只查看安全状态码和白名单错误码,不复制 JWT;
394
411
  7. 代码修改完成后走完整验证、打包、版本、提交、发布和远端回读流程。