@csntgao/uni-base 0.1.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 (90) hide show
  1. package/.prettierignore +4 -0
  2. package/.prettierrc.json +6 -0
  3. package/AGENTS.md +519 -0
  4. package/CLAUDE.md +512 -0
  5. package/README.md +734 -0
  6. package/docs//346/225/260/345/255/227/345/221/230/345/267/245/345/256/214/346/225/264/346/212/275/345/261/211/346/216/245/345/205/245/350/257/264/346/230/216.md +110 -0
  7. package/docs//347/273/204/347/273/207/345/221/230/345/267/245/345/215/225/351/200/211/347/273/204/344/273/266/346/216/245/345/205/245/350/257/264/346/230/216.md +79 -0
  8. package/docs//347/273/204/347/273/207/345/221/230/345/267/245/345/244/232/351/200/211/347/273/204/344/273/266/346/216/245/345/205/245/350/257/264/346/230/216.md +93 -0
  9. package/docs//350/264/246/346/210/267/344/270/216/347/231/273/345/275/225/347/273/204/344/273/266/345/212/250/346/200/201/346/216/245/345/205/245/350/257/264/346/230/216.md +201 -0
  10. package/eslint.config.js +30 -0
  11. package/examples/vanilla/index.html +33 -0
  12. package/examples/vanilla/main.js +247 -0
  13. package/examples/vanilla/package.json +15 -0
  14. package/examples/vanilla/public/runtime-config.js +10 -0
  15. package/examples/vanilla/style.css +99 -0
  16. package/package.json +31 -0
  17. package/packages/core/package.json +30 -0
  18. package/packages/core/src/client.ts +1132 -0
  19. package/packages/core/src/customer-support-client.ts +825 -0
  20. package/packages/core/src/customer-support-types.ts +123 -0
  21. package/packages/core/src/index.ts +79 -0
  22. package/packages/core/src/notification-center-client.ts +462 -0
  23. package/packages/core/src/notification-center-types.ts +154 -0
  24. package/packages/core/src/protocol.ts +237 -0
  25. package/packages/core/src/types.ts +170 -0
  26. package/packages/core/tests/client.test.ts +760 -0
  27. package/packages/core/tests/customer-support-client.test.ts +576 -0
  28. package/packages/core/tests/fake-websocket.ts +55 -0
  29. package/packages/core/tests/notification-center-client.test.ts +467 -0
  30. package/packages/core/tests/protocol.test.ts +124 -0
  31. package/packages/core/tsconfig.json +7 -0
  32. package/packages/core/tsup.config.ts +11 -0
  33. package/packages/web-components/package.json +35 -0
  34. package/packages/web-components/scripts/verify-runtime-build.mjs +111 -0
  35. package/packages/web-components/src/account-context.ts +1540 -0
  36. package/packages/web-components/src/application-switcher-transport.ts +243 -0
  37. package/packages/web-components/src/application-switcher.ts +1103 -0
  38. package/packages/web-components/src/chat.ts +1754 -0
  39. package/packages/web-components/src/customer-support-chat.ts +862 -0
  40. package/packages/web-components/src/customer-support-transport.ts +269 -0
  41. package/packages/web-components/src/index.ts +256 -0
  42. package/packages/web-components/src/notification-center.ts +1066 -0
  43. package/packages/web-components/src/notification-popover.ts +615 -0
  44. package/packages/web-components/src/organization-multi-employee-selector.ts +1564 -0
  45. package/packages/web-components/src/organization-onboarding-transport.ts +54 -0
  46. package/packages/web-components/src/organization-onboarding.ts +1216 -0
  47. package/packages/web-components/src/organization-selector.ts +534 -0
  48. package/packages/web-components/src/organization-single-employee-selector.ts +114 -0
  49. package/packages/web-components/src/prototype-icons.ts +242 -0
  50. package/packages/web-components/src/safe-image-url.ts +29 -0
  51. package/packages/web-components/src/safe-markdown.ts +192 -0
  52. package/packages/web-components/src/uniplat-base-account-context-transport.ts +625 -0
  53. package/packages/web-components/src/uniplat-base-application-switcher-transport.ts +418 -0
  54. package/packages/web-components/src/uniplat-base-application-ticket.ts +254 -0
  55. package/packages/web-components/src/uniplat-base-identity-ticket.ts +202 -0
  56. package/packages/web-components/src/uniplat-base-notification-center-transport.ts +378 -0
  57. package/packages/web-components/src/uniplat-base-organization-multi-employee-selector-transport.ts +394 -0
  58. package/packages/web-components/src/uniplat-base-organization-onboarding-transport.ts +183 -0
  59. package/packages/web-components/src/uniplat-base-organization-single-employee-selector-transport.ts +14 -0
  60. package/packages/web-components/src/user-login-transport.ts +563 -0
  61. package/packages/web-components/src/user-login.ts +2027 -0
  62. package/packages/web-components/src/user-menu.ts +880 -0
  63. package/packages/web-components/tests/account-context.test.ts +798 -0
  64. package/packages/web-components/tests/application-switcher.test.ts +562 -0
  65. package/packages/web-components/tests/chat.test.ts +1130 -0
  66. package/packages/web-components/tests/customer-support-chat.test.ts +428 -0
  67. package/packages/web-components/tests/customer-support-transport.test.ts +79 -0
  68. package/packages/web-components/tests/notification-center-transport.test.ts +271 -0
  69. package/packages/web-components/tests/notification-center.test.ts +429 -0
  70. package/packages/web-components/tests/notification-popover.test.ts +283 -0
  71. package/packages/web-components/tests/organization-multi-employee-selector-transport.test.ts +332 -0
  72. package/packages/web-components/tests/organization-multi-employee-selector.test.ts +225 -0
  73. package/packages/web-components/tests/organization-onboarding.test.ts +260 -0
  74. package/packages/web-components/tests/organization-selector.test.ts +127 -0
  75. package/packages/web-components/tests/organization-single-employee-selector.test.ts +145 -0
  76. package/packages/web-components/tests/uniplat-base-account-context-transport.test.ts +496 -0
  77. package/packages/web-components/tests/uniplat-base-application-switcher-transport.test.ts +598 -0
  78. package/packages/web-components/tests/uniplat-base-application-ticket.test.ts +233 -0
  79. package/packages/web-components/tests/uniplat-base-identity-ticket.test.ts +226 -0
  80. package/packages/web-components/tests/uniplat-base-organization-onboarding-transport.test.ts +227 -0
  81. package/packages/web-components/tests/user-login-transport.test.ts +443 -0
  82. package/packages/web-components/tests/user-login.test.ts +626 -0
  83. package/packages/web-components/tests/user-menu.test.ts +271 -0
  84. package/packages/web-components/tsconfig.json +8 -0
  85. package/packages/web-components/tsup.config.ts +12 -0
  86. package/packages/web-components/tsup.runtime.config.ts +37 -0
  87. package/pnpm-workspace.yaml +6 -0
  88. package/tsconfig.base.json +21 -0
  89. package/vitest.config.ts +20 -0
  90. package//344/272/244/346/216/245/346/226/207/346/241/243.md +254 -0
@@ -0,0 +1,110 @@
1
+ # 数字员工完整抽屉接入说明
2
+
3
+ `<uniplat-ai-employee-chat>` 已包含完整的企业服务顾问右侧抽屉。Host 只负责加载运行时、配置 transport,并在用户点击入口时调用 `open()`。
4
+
5
+ ## Host 接入
6
+
7
+ ```html
8
+ <button id="open-ai-consult" type="button">在线顾问</button>
9
+ <uniplat-ai-employee-chat id="ai-consult"></uniplat-ai-employee-chat>
10
+ ```
11
+
12
+ ```ts
13
+ const chat = document.querySelector('#ai-consult')
14
+
15
+ chat.configure({
16
+ realtimeUrl,
17
+ aeCode,
18
+ agentCode,
19
+ transport,
20
+ })
21
+
22
+ document.querySelector('#open-ai-consult').addEventListener('click', () => chat.open())
23
+ ```
24
+
25
+ 组件初始关闭,不会在配置时创建 WebSocket。调用 `open()` 后,组件自行显示固定右侧抽屉并开始加载会话;组件内部的关闭按钮或 Escape 会调用 `close()` 并释放 WebSocket、心跳和重连任务。Host 如需主动关闭,也可以调用 `chat.close()`。
26
+
27
+ ## Host 必须移除的旧界面
28
+
29
+ Host 不再渲染或包裹以下内容:
30
+
31
+ - 遮罩、侧栏或弹窗容器;
32
+ - 顾问图标、标题、副标题和关闭按钮;
33
+ - 欢迎介绍卡片;
34
+ - 消息列表、连接状态、输入框和发送按钮;
35
+ - “转人工”入口和底部提示。
36
+
37
+ Host 仍负责提供 `realtimeUrl`、`aeCode`、`agentCode` 和 transport。JWT 继续只由 `transport.getAccessToken()` 返回,不得写入组件属性、HTML attribute 或 URL。
38
+
39
+ ## 会话列表
40
+
41
+ 标题左侧按钮由组件内部负责,点击后切换为原型中的“我的会话”界面。面板头部的“开启新对话”按钮只切换到本地草稿,不创建后端 Session。用户发送首条消息时,组件才通过 Host transport 原子创建会话并提交首条消息:
42
+
43
+ ```ts
44
+ const transport = {
45
+ async createSessionWithFirstMessage(request) {
46
+ const response = await hostApi.createSessionWithFirstMessage({
47
+ request_id: request.requestId,
48
+ ae_code: request.aeCode,
49
+ agent_code: request.agentCode,
50
+ content: request.content,
51
+ trusted_context: request.trustedContext,
52
+ attachments: request.attachments.map((attachment) => ({
53
+ transaction_id: attachment.transactionId,
54
+ fspath: attachment.fspath,
55
+ })),
56
+ })
57
+ return {
58
+ sessionId: response.session_id,
59
+ messageId: response.message_id,
60
+ executionId: response.execution_id,
61
+ status: response.status,
62
+ startedAt: response.started_at,
63
+ aeCode: response.ae_code,
64
+ agentCode: response.agent_code,
65
+ conversationType: response.conversation_type,
66
+ title: response.title,
67
+ createdAt: response.created_at,
68
+ }
69
+ },
70
+ async listSessions() {
71
+ return [
72
+ {
73
+ sessionId: '稳定会话 ID',
74
+ title: '企业服务顾问',
75
+ latestMessage: '最后一条普通文本摘要',
76
+ latestMessageId: '最后消息 ID', // 可选,仅用于更新已读状态
77
+ updatedAt: '2026-08-15T01:00:00.000Z',
78
+ unreadCount: 2,
79
+ },
80
+ ]
81
+ },
82
+ async markSessionRead(sessionId, latestMessageId) {
83
+ await hostApi.markSessionRead(sessionId, latestMessageId)
84
+ },
85
+ }
86
+ ```
87
+
88
+ 列表、标题、摘要、相对时间、未读标记、返回、新建和会话切换全部由组件渲染。后端标题为空或仍为默认“新对话”时,组件会用最近消息生成仅用于展示的临时标题;后端返回真实标题或推送 `session_title_updated` 后始终以真实标题为准。点击会话后,组件自行加载该会话历史并更换 WebSocket 订阅,Host 不再处理界面事件。当前选定会话始终视为已读,在聊天界面和“我的会话”列表中都不显示未读角标,也不计入会话入口的聚合未读数;历史加载、列表加载、当前会话收到实时回复以及从会话列表返回时,组件立即清除当前会话的本地未读并异步调用 `markSessionRead()`。历史消息 ID 与列表 `latestMessageId` 不一致时,组件用列表 ID 再同步一次;同步失败不会阻断聊天或恢复当前会话角标,下一次触发会再次同步。其他未选定会话的新回复仍保留未读。未实现列表能力或真实列表暂时加载失败时不会破坏当前聊天,会话面板至少显示组件已绑定的当前会话,并以非阻断提示说明历史列表暂不可用。
89
+
90
+ SDK 生成的 `requestId` 是一次首条消息操作的幂等键。请求失败后,用户不修改内容直接重试时会继续使用同一值;组件重建或同一标签页刷新后,也会通过默认的 `sessionStorage` 恢复该 UUID。存储项只包含 UUID 和首条消息输入的摘要,不包含消息正文、附件内容、JWT 或后端响应;成功、绑定既有会话或主动开启新对话时自动清除。成功响应中的 `sessionId`、`messageId`、`executionId`、`aeCode`、`agentCode` 和 `conversationType` 是权威标识,Host 不得自行生成或替换。`requestId`、`executionId`、JWT 和原始响应不能进入 DOM、公共事件、URL 或日志。
91
+
92
+ `AiEmployeeTransport` 已删除 `getOrCreateSession()` 与 `createSession()`,只接受首条消息原子创建方式。若后端对同一 `request_id` 检测到内容不一致,Host 应抛出 `new AiEmployeeTransportError('IDEMPOTENCY_CONFLICT')`;组件会显示稳定错误并保留当前草稿,用户主动点击“开启新对话”后才生成新的幂等键。
93
+
94
+ ### 0.6 / 0.14 升级说明
95
+
96
+ - 影响:旧 Host 若只实现 `getOrCreateSession()` 或 `createSession()`,升级后会在配置阶段收到 `INVALID_CONFIGURATION`,不会再创建空会话。
97
+ - 迁移:实现必需的 `createSessionWithFirstMessage()`,一次映射 `request_id/ae_code/agent_code/content/trusted_context/attachments`,并返回完整的 `session_id/message_id/execution_id/status/started_at/ae_code/agent_code/conversation_type`;已有会话继续使用原 `sendMessage()`。
98
+ - 风险:Host 若漏传首条正文、每次重试生成新 UUID,或没有把 `409 IDEMPOTENCY_CONFLICT` 映射为稳定错误,可能造成请求失败或重复执行。发布前必须验证打开并关闭抽屉不会产生 Session,同一首条消息的失败重试复用相同 `request_id`,HTTP 与实时事件只显示一条用户消息。
99
+
100
+ ## 动态运行时
101
+
102
+ 调用方继续动态读取 `current.json`,不要锁定版本化模块地址。只要 `configure()` 的 transport 入参保持不变,后续抽屉样式升级不需要调用方重新构建;Host 只需保留组件元素和 `open()` 调用。
103
+
104
+ ## 原型
105
+
106
+ 界面以以下原型的在线顾问抽屉为准:
107
+
108
+ ```text
109
+ /Users/gaostudio/ae_design/1、新平台底座搭建/1、亲亲企服平台(基础能力+企服客户端)/3、需求及UI设计/官网及企服客户端设计/40、亲亲企服官网.html#consult
110
+ ```
@@ -0,0 +1,79 @@
1
+ # 组织员工单选组件接入说明
2
+
3
+ `<uniplat-organization-single-employee-selector>` 提供当前组织内的员工单选能力。它与多选组件复用同一组真实通讯录接口,但使用独立标签、打开参数和标量确认结果。Host 决定打开时机并在确认后调用业务写入接口;组件不写入管理员、所有者或业务角色。
4
+
5
+ ## 配置
6
+
7
+ ```html
8
+ <uniplat-organization-single-employee-selector
9
+ id="organization-single-employee-selector"
10
+ ></uniplat-organization-single-employee-selector>
11
+ ```
12
+
13
+ ```ts
14
+ import {
15
+ createUniplatBaseOrganizationSingleEmployeeSelectorTransport,
16
+ type UniplatOrganizationSingleEmployeeSelectorElement,
17
+ } from '@uniplat/ai-employee-web-components'
18
+
19
+ const selector = document.querySelector<UniplatOrganizationSingleEmployeeSelectorElement>(
20
+ '#organization-single-employee-selector',
21
+ )!
22
+
23
+ selector.configure({
24
+ applicationName: 'OfficialWebsite',
25
+ transport: createUniplatBaseOrganizationSingleEmployeeSelectorTransport({
26
+ serviceBaseUrl: '/api/general/project/uniplat_base/service',
27
+ getAccessToken: () => authStore.getCurrentAccessToken(),
28
+ }),
29
+ })
30
+ ```
31
+
32
+ `applicationName` 必填,用于稳定 Host 接入契约。当前组织仍只由 JWT 决定,Host 不得向组件或 transport 传入 `oid/xid/eid/uid/poid`。
33
+
34
+ ## 打开与确认
35
+
36
+ ```ts
37
+ await selector.open({
38
+ title: '选择组织负责人',
39
+ initialSelectedEmployeeId: currentOwnerEmployeeId,
40
+ disabledSelections: currentOwnerEmployeeId
41
+ ? [{ employeeId: currentOwnerEmployeeId, reason: '组织所有者不能替换' }]
42
+ : [],
43
+ })
44
+
45
+ selector.addEventListener('selection-confirmed', async (event) => {
46
+ await organizationApi.setOwner(event.detail.employeeId, expectedVersion)
47
+ })
48
+
49
+ selector.addEventListener('selection-cancelled', () => {
50
+ // 关闭、取消或 Escape;不写入业务数据。
51
+ })
52
+ ```
53
+
54
+ `selection-confirmed` 的 detail 是标量结果:
55
+
56
+ ```ts
57
+ {
58
+ employeeId: string
59
+ employee: {
60
+ employeeId: string
61
+ displayName: string
62
+ avatar?: { kind: 'file' | 'color' | 'none'; fileId?: string; color?: string }
63
+ maskedMobile?: string
64
+ primaryDepartmentId?: string
65
+ primaryDepartmentName?: string
66
+ departmentIds: string[]
67
+ departmentNames: string[]
68
+ employmentStatus: 'active' | 'pending' | 'rejected' | 'left' | 'unknown'
69
+ }
70
+ }
71
+ ```
72
+
73
+ 单选组件不渲染多选的“已选择成员”右侧栏,候选区占满内容宽度,并使用单选圆点而不是复选框。未选中员工时“确认”不可用;选择另一名员工会替换当前选择。如果初始员工在 `disabledSelections` 中受保护,则不能移除或替换。组件不接受 `selectionMode`、`initialSelectedEmployeeIds`、`maxSelection` 或 `enableDepartmentBulkSelection`。
74
+
75
+ ## 数据与安全
76
+
77
+ 专用 transport 复用 `system.org_directory/tree_full`、`node_contacts`、`contact_search` 和 `contact_get`。候选列表和初始回显只保留在职且启用的员工,`enable=0`、停用或离职员工不会显示。
78
+
79
+ 每次请求都重新调用 `getAccessToken()`,JWT 只进入 `Authorization: Bearer` header,不进入 URL、DOM、公共事件、日志或异常文本。确认事件只返回稳定员工 ID 和脱敏摘要,不包含 JWT、完整手机号、组织身份、transport 或后端原始错误。
@@ -0,0 +1,93 @@
1
+ # 组织员工多选组件接入说明
2
+
3
+ `<uniplat-organization-multi-employee-selector>` 提供当前组织的员工多选能力。Host 负责决定是否显示入口,以及在确认后调用管理员或其他业务写入接口;组件只读取通讯录并返回标准化选择结果。
4
+
5
+ ## 动态运行时接入
6
+
7
+ Host 使用 UniBase 的 `current.json` 加载版本化独立 ESM。只要本文的标签、`configure()`、`open()` 和事件契约不变,UniBase 升级不要求 Host 重新构建。
8
+
9
+ ```html
10
+ <uniplat-organization-multi-employee-selector
11
+ id="organization-employee-selector"
12
+ ></uniplat-organization-multi-employee-selector>
13
+ ```
14
+
15
+ ```ts
16
+ import {
17
+ createUniplatBaseOrganizationMultiEmployeeSelectorTransport,
18
+ type UniplatOrganizationMultiEmployeeSelectorElement,
19
+ } from '@uniplat/ai-employee-web-components'
20
+
21
+ const selector = document.querySelector<UniplatOrganizationMultiEmployeeSelectorElement>(
22
+ '#organization-employee-selector',
23
+ )!
24
+
25
+ selector.configure({
26
+ applicationName: 'OfficialWebsite',
27
+ transport: createUniplatBaseOrganizationMultiEmployeeSelectorTransport({
28
+ serviceBaseUrl: '/api/general/project/uniplat_base/service',
29
+ getAccessToken: () => authStore.getCurrentAccessToken(),
30
+ }),
31
+ })
32
+ ```
33
+
34
+ `applicationName` 是稳定的公共配置字段;当前组织目录接口不需要应用参数,因此专用 transport 不会把它补入请求。当前组织只能由 JWT 决定。
35
+
36
+ ## 打开、回显和确认
37
+
38
+ ```ts
39
+ await selector.open({
40
+ title: '选择组织管理员',
41
+ initialSelectedEmployeeIds: currentAdministrators.map(({ employeeId }) => employeeId),
42
+ disabledSelections: owner
43
+ ? [{ employeeId: owner.employeeId, reason: '组织所有者不能在此处移除' }]
44
+ : [],
45
+ maxSelection: 20,
46
+ })
47
+
48
+ selector.addEventListener('selection-confirmed', async (event) => {
49
+ await organizationApi.setAdministrators(event.detail.employeeIds, expectedVersion)
50
+ })
51
+
52
+ selector.addEventListener('selection-cancelled', () => {
53
+ // 关闭、取消或 Escape;不写入业务数据。
54
+ })
55
+
56
+ selector.addEventListener('selector-error', (event) => {
57
+ reportSafeSelectorError(event.detail.code, event.detail.retryable)
58
+ })
59
+ ```
60
+
61
+ `selection-confirmed` 的 detail 为:
62
+
63
+ ```ts
64
+ {
65
+ employeeIds: string[]
66
+ employees: Array<{
67
+ employeeId: string
68
+ displayName: string
69
+ avatar?: { kind: 'file' | 'color' | 'none'; fileId?: string; color?: string }
70
+ maskedMobile?: string
71
+ primaryDepartmentId?: string
72
+ primaryDepartmentName?: string
73
+ departmentIds: string[]
74
+ departmentNames: string[]
75
+ employmentStatus: 'active' | 'pending' | 'rejected' | 'left' | 'unknown'
76
+ }>
77
+ }
78
+ ```
79
+
80
+ 事件只包含业务写入所需的稳定员工 ID 与脱敏摘要。组件不会返回 JWT、完整手机号、组织身份、内部用户 ID、transport 或后端错误正文。
81
+
82
+ ## 数据与安全边界
83
+
84
+ 专用 transport 复用以下现有接口:
85
+
86
+ - `system.org_directory/tree_full`
87
+ - `system.org_directory/node_contacts`
88
+ - `system.org_directory/contact_search`
89
+ - `system.org_directory/contact_get`
90
+
91
+ 每次 HTTP 请求都会重新调用 `getAccessToken()`;JWT 只进入 `Authorization: Bearer` header。请求固定使用 `directory_scope=organization`,不得由 Host 传入 `oid`、`xid`、`eid`、`uid`、`poid` 或其他身份字段。
92
+
93
+ v0.1 只选择在职且启用的员工并固定为多选;`enable=0`、停用或离职员工不会进入候选列表和初始选择回显。`maxSelection` 如提供必须不小于 2;部门仅用于导航,`enableDepartmentBulkSelection=true` 在当前版本会被拒绝。单选员工请使用独立的 `<uniplat-organization-single-employee-selector>`,不通过本组件模拟。
@@ -0,0 +1,201 @@
1
+ # 账户与登录组件动态接入说明
2
+
3
+ 本文面向网页、Vue、Nuxt 等调用方,只说明动态运行时接入 `<uniplat-account-context>` 与 `<uniplat-user-login>` 的稳定契约。
4
+
5
+ 调用方只要持续使用本文的标签、`configure()` 入参和事件名,UniBase 后续仅升级运行时版本时无需同步改代码、重建或重新发包。
6
+
7
+ ## 1. 运行时加载(固定)
8
+
9
+ 不要把某个版本号写死在页面中。每次页面初始化读取 `current.json`,再加载其 `module_url`:
10
+
11
+ ```js
12
+ async function loadUniBaseRuntime(runtimeBaseUrl) {
13
+ const manifestUrl = new URL('current.json', `${runtimeBaseUrl.replace(/\/$/, '')}/`)
14
+ const manifest = await fetch(manifestUrl, { cache: 'no-store' }).then((response) => {
15
+ if (!response.ok) throw new Error('UniBase runtime is unavailable')
16
+ return response.json()
17
+ })
18
+ return import(new URL(manifest.module_url, manifestUrl).href)
19
+ }
20
+
21
+ const uniBase = await loadUniBaseRuntime('https://10.10.10.131:8787')
22
+ ```
23
+
24
+ 运行时会自行注册 Custom Element。调用方不需要也不应手动 `customElements.define()`。
25
+
26
+ 运行时服务要求:`current.json` 可匿名读取且不缓存;版本目录可长期缓存;允许调用方 Origin 的 CORS。当前版本运行时必须保留稳定导出 `createUniplatBaseAccountContextTransport`、`createUniplatBaseApplicationSwitcherTransport`、`createUniplatBaseOrganizationOnboardingTransport` 和 `createUniplatBaseUserLoginTransport`。
27
+
28
+ ## 2. 固定安全边界
29
+
30
+ - JWT 只能由调用方的 `getAccessToken()` 读取,或由 `setAccessToken()` 保存;不能写进 HTML attribute、URL、日志、事件 detail 或组件配置对象。
31
+ - 两个组件均通过 JavaScript `configure()` 方法接收对象;不要把对象或 JWT 放进 HTML attribute。
32
+ - 调用方不传姓名、头像、手机号、当前组织或组织列表给 Account Context;组件通过其 transport 自行加载。
33
+ - 组件升级时,调用方只读取新的 `current.json`。调用方不应依赖 Shadow DOM 内部类名、DOM 结构、私有方法或未列出的事件。
34
+
35
+ ## 3. `<uniplat-account-context>`
36
+
37
+ 该组件负责导航右侧的工作台入口、账户胶囊、账户菜单、组织切换和内置创建企业/团队流程。它不渲染网站品牌或整条导航。
38
+
39
+ ### 3.1 HTML 容器(固定)
40
+
41
+ ```html
42
+ <uniplat-account-context id="account-context"></uniplat-account-context>
43
+ ```
44
+
45
+ ### 3.2 配置入参(固定)
46
+
47
+ ```js
48
+ const accountContext = document.querySelector('#account-context')
49
+
50
+ const accountTransport = uniBase.createUniplatBaseAccountContextTransport({
51
+ serviceBaseUrl: 'https://gateway.example.com/api/general/project/uniplat_base/service',
52
+ getAccessToken: () => authStore.getAccessToken(),
53
+ setAccessToken: (nextToken, projection) => authStore.setAccessToken(nextToken, projection),
54
+ })
55
+
56
+ const workbenchTransport = uniBase.createUniplatBaseApplicationSwitcherTransport({
57
+ serviceBaseUrl: 'https://gateway.example.com/api/general/project/uniplat_base/service',
58
+ })
59
+
60
+ accountContext.configure({
61
+ applicationName: 'OfficialWebsite',
62
+ transport: accountTransport,
63
+ workbench: {
64
+ getAccessToken: () => authStore.getAccessToken(),
65
+ transport: workbenchTransport,
66
+ },
67
+ organizationOnboarding: {
68
+ transport: uniBase.createUniplatBaseOrganizationOnboardingTransport({
69
+ serviceBaseUrl: 'https://gateway.example.com/api/general/project/uniplat_base/service',
70
+ getAccessToken: () => authStore.getAccessToken(),
71
+ }),
72
+ },
73
+ })
74
+ ```
75
+
76
+ | 入参 | 是否必填 | 调用方职责 |
77
+ | ------------------------ | -------- | ------------------------------------------------------------------ |
78
+ | `applicationName` | 是 | 当前应用的稳定名称,如 `OfficialWebsite`;不得从 JWT 或 URL 推断。 |
79
+ | `transport` | 是 | 提供当前 JWT 的读取、组织切换后 JWT 保存和账户/组织投影请求。 |
80
+ | `workbench` | 否 | 需要工作台九宫格时提供;与账户资料加载相互独立。 |
81
+ | `organizationOnboarding` | 否 | 需要在组件内创建团队/企业时提供。 |
82
+ | `showWorkbench` | 否 | 默认为 `true`;设为 `false` 隐藏工作台入口。 |
83
+ | `showVerificationStatus` | 否 | 默认为 `true`;设为 `false` 隐藏认证状态视觉元素。 |
84
+
85
+ 组件自行从 `list_available_applications` 和 `list_available_organizations` 加载资料。后者顶层的 `display_name`、`avatar_url`、`mobile_masked` 是用户资料来源;当前组织的员工姓名和头像优先显示。用户打开“我的员工信息”弹窗时,专用 transport 会按当前 JWT 额外请求当前员工主部门;这一按需请求不改变调用方配置。调用方无需因资料字段或内部接口优化改变配置。
86
+
87
+ ### 3.3 输出事件与调用方处理(固定)
88
+
89
+ ```js
90
+ accountContext.addEventListener('organization-selected', async (event) => {
91
+ await accountContext.switchOrganization(event.detail.organizationId)
92
+ })
93
+
94
+ accountContext.addEventListener('logout-requested', () => {
95
+ authStore.clearAccessToken()
96
+ closeAuthenticatedUi()
97
+ })
98
+
99
+ accountContext.addEventListener('employee-info-requested', () => {
100
+ // 可选:记录业务埋点。组件已自行打开“我的员工信息”弹窗。
101
+ })
102
+
103
+ accountContext.addEventListener('enterprise-authentication-requested', (event) => {
104
+ openEnterpriseAuthentication(event.detail.organizationId)
105
+ })
106
+ ```
107
+
108
+ | 事件 | `detail` 中可用数据 | 调用方动作 |
109
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------- |
110
+ | `organization-selected` | `organizationId` | 必须调用 `switchOrganization(organizationId)`,由组件 transport 换取并保存新 JWT。 |
111
+ | `logout-requested` | 无凭据业务数据 | 清理调用方登录态、关闭或跳转页面。 |
112
+ | `employee-info-requested` | 业务标识 | 可选:记录埋点或附加业务;组件已自行打开员工信息弹窗。 |
113
+ | `account-settings-requested` | 业务标识 | 仅自定义 transport 时按需处理;UniplatBase 专用 transport 会自行签 ticket 并新开个人设置页。 |
114
+ | `enterprise-authentication-requested`、`enterprise-authentication-deferred`、`team-upgrade-requested`、`team-invite-members-requested`、`join-organization-requested` | 业务标识 | 打开相应调用方业务流程。 |
115
+ | `error` | `code`、安全提示 | 显示安全提示;不得展示或记录原始后端响应。 |
116
+
117
+ 创建团队/企业不需要调用方创建另一个组件:账户组件会启动内部流程;成功后自行刷新组织列表。
118
+
119
+ ## 4. `<uniplat-user-login>`
120
+
121
+ 该组件提供密码、验证码、扫码三个固定页签以及新用户入驻过渡状态。组件不管理外层弹窗开关,也不保存 JWT。
122
+
123
+ ### 4.1 HTML 容器与配置入参(固定)
124
+
125
+ ```html
126
+ <uniplat-user-login id="user-login"></uniplat-user-login>
127
+ ```
128
+
129
+ ```js
130
+ const userLogin = document.querySelector('#user-login')
131
+
132
+ userLogin.configure({
133
+ transport: uniBase.createUniplatBaseUserLoginTransport({
134
+ apiBaseUrl: 'https://gateway.example.com/api',
135
+ setAccessToken: (nextToken, metadata) => authStore.completeLogin(nextToken, metadata),
136
+ }),
137
+ applicationName: 'OfficialWebsite',
138
+ headerTitle: '登录企业工作台',
139
+ headerSubtitle: '手机号验证后进入企业服务平台',
140
+ headerIconUrl: 'https://static.example.com/login-shield.svg',
141
+ securityHint: '登录凭证仅用于企业工作台认证,请勿向他人透露验证码',
142
+ userAgreementUrl: '/agreements/user',
143
+ privacyAgreementUrl: '/agreements/privacy',
144
+ forgotPasswordUrl: '/account/forgot-password',
145
+ initialMethod: 'verification-code',
146
+ })
147
+ ```
148
+
149
+ | 入参 | 是否必填 | 调用方职责 |
150
+ | ------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------- |
151
+ | `transport` | 是 | 完成登录接口调用;专用 transport 的 `setAccessToken()` 必须原子保存 JWT。 |
152
+ | `applicationName` | 是 | 目标应用稳定名称。 |
153
+ | `headerTitle`、`headerSubtitle`、`headerIconUrl`、`securityHint` | 否 | 外观文案与图标;不得包含凭据。 |
154
+ | `userAgreementUrl`、`privacyAgreementUrl`、`forgotPasswordUrl` | 否 | 调用方页面地址。 |
155
+ | `initialMethod` | 否 | `password`、`verification-code` 或 `qr-code`。 |
156
+ | `countdownSeconds`、`qrPollIntervalMs`、`initiallyAgreed`、`initiallyRememberSession` | 否 | 固定行为的可选初始值。 |
157
+
158
+ 自定义 transport 未提供密码或扫码能力时,相应页签保持展示但禁用;使用 `createUniplatBaseUserLoginTransport()` 时三种方式均可用。验证码登录只发送手机号和短信验证码,不使用图形验证码。专用 transport 自动将 `applicationName` 同时传为 `application_name` 与 `client_id`,并从浏览器收集 `device_type`、`device_name`、`device_brand`;`device_id` 由浏览器与设备特征在本地计算 SHA-256 摘要,调用方无需增加配置。原始特征不会上报,SDK 不写入 localStorage、Cookie 或其他宿主存储。若调用方已有合规、稳定的设备标识,可在 factory 的可选 `deviceInfo.deviceId` 中覆盖默认值。
159
+
160
+ ### 4.2 输出事件与调用方处理(固定)
161
+
162
+ ```js
163
+ userLogin.addEventListener('login-success', () => {
164
+ closeLoginModal()
165
+ renderAuthenticatedUi()
166
+ })
167
+
168
+ userLogin.addEventListener('onboarding-requested', () => {
169
+ document.querySelector('#account-context')?.startOrganizationOnboarding()
170
+ })
171
+
172
+ userLogin.addEventListener('agreement-requested', (event) => {
173
+ openAgreement(event.detail.type)
174
+ })
175
+
176
+ userLogin.addEventListener('forgot-password-requested', () => {
177
+ openForgotPasswordPage()
178
+ })
179
+ ```
180
+
181
+ | 事件 | `detail` 中可用数据 | 调用方动作 |
182
+ | --------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------ |
183
+ | `login-success` | `applicationName`、可选 `isNewUser`;不含 JWT | 关闭宿主登录弹窗,刷新登录后 UI。JWT 已先经 `setAccessToken()` 保存。 |
184
+ | `onboarding-requested` | `applicationName`、`trigger`(`automatic` 或 `manual`) | 调用 Account Context 的 `startOrganizationOnboarding()` 或进入等效流程。 |
185
+ | `sms-sent` | 空对象 | 可选显示“验证码已发送”。 |
186
+ | `agreement-requested` | `type`(`user` 或 `privacy`) | 打开对应协议页。 |
187
+ | `forgot-password-requested` | 空对象 | 打开找回密码页。 |
188
+ | `error` | `code`、安全提示 | 显示安全提示;不得显示原始接口错误。 |
189
+
190
+ `login-success` 只表示认证与 JWT 保存已完成。关闭外层登录弹窗始终是调用方职责,不能期待组件自行移除宿主弹窗。
191
+
192
+ ## 5. 不升级调用方的保证范围
193
+
194
+ 满足以下条件时,UniBase 可独立升级运行时:
195
+
196
+ 1. 调用方动态读取 `current.json`,而不是锁定版本化模块地址;
197
+ 2. 保持上述标签名、`configure()` 入参、transport factory 参数和事件名不变;
198
+ 3. 不依赖组件私有 DOM、样式类、未文档化方法或事件;
199
+ 4. 后端接口仍遵守既有 UniplatBase 契约。
200
+
201
+ 如果未来必须修改上述公开契约,UniBase 会作为破坏性变更单独发布迁移说明;不会以常规运行时升级静默要求调用方修改代码。
@@ -0,0 +1,30 @@
1
+ import eslint from '@eslint/js'
2
+ import globals from 'globals'
3
+ import tseslint from 'typescript-eslint'
4
+
5
+ export default tseslint.config(
6
+ {
7
+ ignores: ['**/dist/**', '**/node_modules/**', 'coverage/**'],
8
+ },
9
+ eslint.configs.recommended,
10
+ ...tseslint.configs.recommended,
11
+ {
12
+ files: ['**/*.ts'],
13
+ languageOptions: {
14
+ globals: {
15
+ ...globals.browser,
16
+ ...globals.node,
17
+ },
18
+ },
19
+ rules: {
20
+ '@typescript-eslint/consistent-type-imports': 'error',
21
+ '@typescript-eslint/no-explicit-any': 'error',
22
+ },
23
+ },
24
+ {
25
+ files: ['examples/**/*.js'],
26
+ languageOptions: {
27
+ globals: globals.browser,
28
+ },
29
+ },
30
+ )
@@ -0,0 +1,33 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="Uniplat 数字员工 Web Component 原生页面示例" />
7
+ <title>数字员工 SDK 示例</title>
8
+ <link rel="stylesheet" href="/style.css" />
9
+ </head>
10
+ <body>
11
+ <main class="page">
12
+ <section class="introduction">
13
+ <p class="eyebrow">Uniplat AI Employee SDK</p>
14
+ <h1>把数字员工嵌入任意网页</h1>
15
+ <p>
16
+ 这个页面没有使用 Vue、React 或其他 UI 框架。请先在
17
+ <code>runtime-config.js</code> 中填写宿主系统提供的接口地址。
18
+ </p>
19
+ </section>
20
+
21
+ <section class="component-showcase" aria-label="Web Component 示例">
22
+ <uniplat-user-login id="user-login"></uniplat-user-login>
23
+ <uniplat-organization-selector id="organization-selector"></uniplat-organization-selector>
24
+ <uniplat-account-context id="account-context"></uniplat-account-context>
25
+ <button id="open-employee-chat" type="button">打开企业服务顾问</button>
26
+ <uniplat-ai-employee-chat id="employee-chat"></uniplat-ai-employee-chat>
27
+ </section>
28
+ </main>
29
+
30
+ <script src="/runtime-config.js"></script>
31
+ <script type="module" src="/main.js"></script>
32
+ </body>
33
+ </html>