@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.
- package/.prettierignore +4 -0
- package/.prettierrc.json +6 -0
- package/AGENTS.md +519 -0
- package/CLAUDE.md +512 -0
- package/README.md +734 -0
- 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
- 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
- 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
- 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
- package/eslint.config.js +30 -0
- package/examples/vanilla/index.html +33 -0
- package/examples/vanilla/main.js +247 -0
- package/examples/vanilla/package.json +15 -0
- package/examples/vanilla/public/runtime-config.js +10 -0
- package/examples/vanilla/style.css +99 -0
- package/package.json +31 -0
- package/packages/core/package.json +30 -0
- package/packages/core/src/client.ts +1132 -0
- package/packages/core/src/customer-support-client.ts +825 -0
- package/packages/core/src/customer-support-types.ts +123 -0
- package/packages/core/src/index.ts +79 -0
- package/packages/core/src/notification-center-client.ts +462 -0
- package/packages/core/src/notification-center-types.ts +154 -0
- package/packages/core/src/protocol.ts +237 -0
- package/packages/core/src/types.ts +170 -0
- package/packages/core/tests/client.test.ts +760 -0
- package/packages/core/tests/customer-support-client.test.ts +576 -0
- package/packages/core/tests/fake-websocket.ts +55 -0
- package/packages/core/tests/notification-center-client.test.ts +467 -0
- package/packages/core/tests/protocol.test.ts +124 -0
- package/packages/core/tsconfig.json +7 -0
- package/packages/core/tsup.config.ts +11 -0
- package/packages/web-components/package.json +35 -0
- package/packages/web-components/scripts/verify-runtime-build.mjs +111 -0
- package/packages/web-components/src/account-context.ts +1540 -0
- package/packages/web-components/src/application-switcher-transport.ts +243 -0
- package/packages/web-components/src/application-switcher.ts +1103 -0
- package/packages/web-components/src/chat.ts +1754 -0
- package/packages/web-components/src/customer-support-chat.ts +862 -0
- package/packages/web-components/src/customer-support-transport.ts +269 -0
- package/packages/web-components/src/index.ts +256 -0
- package/packages/web-components/src/notification-center.ts +1066 -0
- package/packages/web-components/src/notification-popover.ts +615 -0
- package/packages/web-components/src/organization-multi-employee-selector.ts +1564 -0
- package/packages/web-components/src/organization-onboarding-transport.ts +54 -0
- package/packages/web-components/src/organization-onboarding.ts +1216 -0
- package/packages/web-components/src/organization-selector.ts +534 -0
- package/packages/web-components/src/organization-single-employee-selector.ts +114 -0
- package/packages/web-components/src/prototype-icons.ts +242 -0
- package/packages/web-components/src/safe-image-url.ts +29 -0
- package/packages/web-components/src/safe-markdown.ts +192 -0
- package/packages/web-components/src/uniplat-base-account-context-transport.ts +625 -0
- package/packages/web-components/src/uniplat-base-application-switcher-transport.ts +418 -0
- package/packages/web-components/src/uniplat-base-application-ticket.ts +254 -0
- package/packages/web-components/src/uniplat-base-identity-ticket.ts +202 -0
- package/packages/web-components/src/uniplat-base-notification-center-transport.ts +378 -0
- package/packages/web-components/src/uniplat-base-organization-multi-employee-selector-transport.ts +394 -0
- package/packages/web-components/src/uniplat-base-organization-onboarding-transport.ts +183 -0
- package/packages/web-components/src/uniplat-base-organization-single-employee-selector-transport.ts +14 -0
- package/packages/web-components/src/user-login-transport.ts +563 -0
- package/packages/web-components/src/user-login.ts +2027 -0
- package/packages/web-components/src/user-menu.ts +880 -0
- package/packages/web-components/tests/account-context.test.ts +798 -0
- package/packages/web-components/tests/application-switcher.test.ts +562 -0
- package/packages/web-components/tests/chat.test.ts +1130 -0
- package/packages/web-components/tests/customer-support-chat.test.ts +428 -0
- package/packages/web-components/tests/customer-support-transport.test.ts +79 -0
- package/packages/web-components/tests/notification-center-transport.test.ts +271 -0
- package/packages/web-components/tests/notification-center.test.ts +429 -0
- package/packages/web-components/tests/notification-popover.test.ts +283 -0
- package/packages/web-components/tests/organization-multi-employee-selector-transport.test.ts +332 -0
- package/packages/web-components/tests/organization-multi-employee-selector.test.ts +225 -0
- package/packages/web-components/tests/organization-onboarding.test.ts +260 -0
- package/packages/web-components/tests/organization-selector.test.ts +127 -0
- package/packages/web-components/tests/organization-single-employee-selector.test.ts +145 -0
- package/packages/web-components/tests/uniplat-base-account-context-transport.test.ts +496 -0
- package/packages/web-components/tests/uniplat-base-application-switcher-transport.test.ts +598 -0
- package/packages/web-components/tests/uniplat-base-application-ticket.test.ts +233 -0
- package/packages/web-components/tests/uniplat-base-identity-ticket.test.ts +226 -0
- package/packages/web-components/tests/uniplat-base-organization-onboarding-transport.test.ts +227 -0
- package/packages/web-components/tests/user-login-transport.test.ts +443 -0
- package/packages/web-components/tests/user-login.test.ts +626 -0
- package/packages/web-components/tests/user-menu.test.ts +271 -0
- package/packages/web-components/tsconfig.json +8 -0
- package/packages/web-components/tsup.config.ts +12 -0
- package/packages/web-components/tsup.runtime.config.ts +37 -0
- package/pnpm-workspace.yaml +6 -0
- package/tsconfig.base.json +21 -0
- package/vitest.config.ts +20 -0
- package//344/272/244/346/216/245/346/226/207/346/241/243.md +254 -0
package/README.md
ADDED
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
# uni-base
|
|
2
|
+
|
|
3
|
+
用于把 Uniplat 登录、应用切换和数字员工聊天能力嵌入第三方网页的 TypeScript SDK。v0.1 包含与 UI 无关的浏览器客户端、一组标准 Web Component,以及原生 HTML 接入示例。
|
|
4
|
+
|
|
5
|
+
## Workspace
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
packages/core @uniplat/ai-employee-client
|
|
9
|
+
packages/web-components @uniplat/ai-employee-web-components
|
|
10
|
+
examples/vanilla 无框架接入示例
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
要求 Node.js 20+ 和 pnpm。常用命令:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm install
|
|
17
|
+
pnpm lint
|
|
18
|
+
pnpm format:check
|
|
19
|
+
pnpm typecheck
|
|
20
|
+
pnpm test
|
|
21
|
+
pnpm build
|
|
22
|
+
pnpm dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`pnpm dev` 会先构建两个 SDK 包,再启动 `examples/vanilla`。示例通过 workspace 包的 `exports` 加载 `dist`,不引用包内源码;宿主接口地址在 `examples/vanilla/public/runtime-config.js` 中配置。
|
|
26
|
+
|
|
27
|
+
## Core client
|
|
28
|
+
|
|
29
|
+
宿主系统负责用自己的 HTTP API 适配会话、历史、发送和短期令牌。SDK 不调用 Kanban 内部 HMAC 接口,也不会持久化 JWT。
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import {
|
|
33
|
+
AiEmployeeClient,
|
|
34
|
+
AiEmployeeTransportError,
|
|
35
|
+
type AiEmployeeTransport,
|
|
36
|
+
} from '@uniplat/ai-employee-client'
|
|
37
|
+
|
|
38
|
+
const transport: AiEmployeeTransport = {
|
|
39
|
+
async createSessionWithFirstMessage(request) {
|
|
40
|
+
const response = await hostApi.createSessionWithFirstMessage({
|
|
41
|
+
request_id: request.requestId,
|
|
42
|
+
ae_code: request.aeCode,
|
|
43
|
+
agent_code: request.agentCode,
|
|
44
|
+
content: request.content,
|
|
45
|
+
trusted_context: request.trustedContext,
|
|
46
|
+
attachments: request.attachments.map((attachment) => ({
|
|
47
|
+
transaction_id: attachment.transactionId,
|
|
48
|
+
fspath: attachment.fspath,
|
|
49
|
+
})),
|
|
50
|
+
})
|
|
51
|
+
return {
|
|
52
|
+
sessionId: response.session_id,
|
|
53
|
+
messageId: response.message_id,
|
|
54
|
+
executionId: response.execution_id,
|
|
55
|
+
status: response.status,
|
|
56
|
+
startedAt: response.started_at,
|
|
57
|
+
aeCode: response.ae_code,
|
|
58
|
+
agentCode: response.agent_code,
|
|
59
|
+
conversationType: response.conversation_type,
|
|
60
|
+
title: response.title,
|
|
61
|
+
createdAt: response.created_at,
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
async listMessages(sessionId) {
|
|
65
|
+
return hostApi.listMessages(sessionId)
|
|
66
|
+
},
|
|
67
|
+
async listSessions() {
|
|
68
|
+
return hostApi.listSessions()
|
|
69
|
+
},
|
|
70
|
+
async markSessionRead(sessionId, latestMessageId) {
|
|
71
|
+
await hostApi.markSessionRead(sessionId, latestMessageId)
|
|
72
|
+
},
|
|
73
|
+
async sendMessage(sessionId, content) {
|
|
74
|
+
const result = await hostApi.sendMessage(sessionId, content)
|
|
75
|
+
if (result.status === 429) {
|
|
76
|
+
throw new AiEmployeeTransportError('TOO_MANY_REQUESTS')
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
async getAccessToken() {
|
|
80
|
+
// 可以返回已登录用户 JWT,也可以返回 Host 为匿名访客申请的匿名 JWT。
|
|
81
|
+
return auth.getCurrentAccessToken()
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const client = new AiEmployeeClient({
|
|
86
|
+
realtimeUrl: 'wss://kanban.example.com/api/user-realtime/ws',
|
|
87
|
+
aeCode: 'recruit',
|
|
88
|
+
agentCode: 'default',
|
|
89
|
+
transport,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
client.on('message', ({ message, source }) => {
|
|
93
|
+
renderMessage(message, source)
|
|
94
|
+
})
|
|
95
|
+
client.on('connection-state-change', ({ current }) => {
|
|
96
|
+
renderConnectionState(current)
|
|
97
|
+
})
|
|
98
|
+
client.on('error', ({ code, message }) => {
|
|
99
|
+
renderSafeError(code, message)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
await client.start() // 未传 sessionId 时只建立本地草稿,不创建后端空 Session
|
|
103
|
+
await client.sendMessage('帮我筛选候选人')
|
|
104
|
+
|
|
105
|
+
// 页面或业务容器销毁时:
|
|
106
|
+
client.destroy()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
也可以在构造时传入 `sessionId` 绑定已有会话,稍后调用 `bindSession(sessionId)` 切换,或调用 `startNewConversation()` 回到本地草稿。未绑定会话时,`sendMessage()` 会生成 UUID `request_id` 并调用 `createSessionWithFirstMessage()`;Host 将字段映射到首条消息原子创建接口,成功后 SDK 才使用响应中的 `sessionId` 和 `messageId` 绑定会话。网络失败、组件重建或同一标签页刷新后,重试内容未变化的首条消息会复用原 `request_id`,防止重复会话和重复执行。默认恢复信息只在 `sessionStorage` 保存 UUID 和消息内容的摘要,不保存消息正文、附件内容、JWT 或后端响应;成功、切换既有会话或主动开启新对话后自动清除。测试或特殊 Host 可以通过 `firstMessageRequestStore` 注入等价存储,传 `null` 可关闭页面恢复。
|
|
110
|
+
|
|
111
|
+
`AiEmployeeTransport` 不再提供 `getOrCreateSession()` 或 `createSession()`;Host 不得创建空 Session,也不得先创建会话再调用后续消息接口。已有会话的第二条及以后消息仍通过 `sendMessage(sessionId, content)` 提交。
|
|
112
|
+
|
|
113
|
+
`getAccessToken()` 的返回值是当前访问主体的连接凭证:它既可以是注册用户 JWT,也可以是 Host 通过匿名身份接口取得的匿名 JWT。SDK 不判断或持久化主体类型;每次连接和 `4401` 恢复都会重新调用该方法。匿名 JWT 与注册用户 JWT 遵守同一安全边界,只进入 HTTP `Authorization` header 和 WebSocket `authenticate` 首帧,不进入 URL、DOM、事件或日志。断线后客户端先重新拉取 HTTP 历史,再取得当前凭证并恢复订阅;历史拉取失败时会请求 WebSocket replay 兜底。
|
|
114
|
+
|
|
115
|
+
宿主 transport 遇到稳定后端错误时,可抛出 `AiEmployeeTransportError`,或抛出带有同名 `code` 的宿主错误。客户端只识别 `UNAUTHORIZED`、`ACCESS_DENIED`、`AI_EMPLOYEE_NOT_AVAILABLE`、`APPLICATION_NOT_SUPPORTED`、`ORGANIZATION_CONTEXT_REQUIRED`、`INVALID_ARGUMENT`、`IDEMPOTENCY_CONFLICT`、`REALTIME_UNAVAILABLE` 和 `TOO_MANY_REQUESTS` 白名单,并映射为主体中性的安全提示;其他错误码和后端正文不会透传。`IDEMPOTENCY_CONFLICT` 映射为不可自动重试的 `FIRST_MESSAGE_CONFLICT`,提示用户主动开启新对话;频率限制映射为 `RATE_LIMITED`。
|
|
116
|
+
|
|
117
|
+
## Web Component
|
|
118
|
+
|
|
119
|
+
调用方只接入登录和统一账户工作台时,请先阅读 [账户与登录组件动态接入说明](docs/账户与登录组件动态接入说明.md)。该文档定义了稳定的配置、事件与动态运行时加载方式;在公开契约不变的常规升级中,调用方无需改代码或重新构建。
|
|
120
|
+
|
|
121
|
+
安装并导入组件包后,通过 JavaScript property 配置。不要把 JWT 写入 attribute。
|
|
122
|
+
|
|
123
|
+
当前包提供十个 Web Component:
|
|
124
|
+
|
|
125
|
+
```html
|
|
126
|
+
<uniplat-ai-employee-chat></uniplat-ai-employee-chat>
|
|
127
|
+
<uniplat-customer-support-chat></uniplat-customer-support-chat>
|
|
128
|
+
<uniplat-user-login></uniplat-user-login>
|
|
129
|
+
<uniplat-organization-selector></uniplat-organization-selector>
|
|
130
|
+
<uniplat-organization-multi-employee-selector></uniplat-organization-multi-employee-selector>
|
|
131
|
+
<uniplat-organization-single-employee-selector></uniplat-organization-single-employee-selector>
|
|
132
|
+
<uniplat-account-context></uniplat-account-context>
|
|
133
|
+
<uniplat-user-menu></uniplat-user-menu>
|
|
134
|
+
<uniplat-notification-center></uniplat-notification-center>
|
|
135
|
+
<uniplat-notification-popover></uniplat-notification-popover>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 浏览器运行时发布
|
|
139
|
+
|
|
140
|
+
`pnpm --filter @uniplat/ai-employee-web-components build` 除 npm 包的 `dist/` 外,还会生成可由官网运行时加载的独立浏览器 ESM:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
dist/runtime/current.json
|
|
144
|
+
dist/runtime/releases/<version>/index.js
|
|
145
|
+
dist/runtime/releases/<version>/index.js.map
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
运行时版本从 Web Components 的 `package.json` 读取。构建会实际导入独立 ESM,校验 Host 必需导出、九个 Custom Element 注册结果,并拒绝任何残留的运行时 import;全部通过后才原子写入本地 `current.json`。独立 ESM 会完整打包 `lit` 和 `@uniplat/ai-employee-client`。
|
|
149
|
+
|
|
150
|
+
部署时仍须先上传并验证整个版本目录,再原子替换远端 `current.json`;清单使用 `Cache-Control: no-store`,版本目录使用 `Cache-Control: public, max-age=31536000, immutable`。同一版本的文件不得覆盖,回滚只切回先前清单。缓存头、匿名 CORS、CSP 与远端原子替换由静态发布平台负责,不是 JavaScript 构建产物能够代替的配置。
|
|
151
|
+
|
|
152
|
+
生产静态服务必须允许调用方 origin 以匿名 CORS 加载清单与模块。清单契约及官网校验规则见 `/Users/gaostudio/.vibe-requirements/qqxbwebsite_UniBase运行时自动升级需求_v0_1.md`。
|
|
153
|
+
|
|
154
|
+
### Notification Center 与小型通知浮层
|
|
155
|
+
|
|
156
|
+
完整通知页使用 `<uniplat-notification-center>`;导航栏铃铛下方的 40 号官网原型小浮层使用 `<uniplat-notification-popover>`。两者复用同一个 `NotificationCenterTransport` 和 UniplatBase transport factory,小浮层不新增接口,也不创建第二条 WebSocket。
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import {
|
|
160
|
+
createUniplatBaseNotificationCenterTransport,
|
|
161
|
+
type UniplatNotificationCenter,
|
|
162
|
+
type UniplatNotificationPopover,
|
|
163
|
+
} from '@uniplat/ai-employee-web-components'
|
|
164
|
+
|
|
165
|
+
const transport = createUniplatBaseNotificationCenterTransport({
|
|
166
|
+
serviceBaseUrl,
|
|
167
|
+
getAccessToken,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const popover = document.querySelector<UniplatNotificationPopover>('uniplat-notification-popover')!
|
|
171
|
+
const center = document.querySelector<UniplatNotificationCenter>('uniplat-notification-center')!
|
|
172
|
+
|
|
173
|
+
popover.configure({ applicationName, transport })
|
|
174
|
+
center.configure({ applicationName, transport })
|
|
175
|
+
|
|
176
|
+
notificationBell.addEventListener('click', () => popover.toggle())
|
|
177
|
+
popover.addEventListener('notification-center-requested', (event) => {
|
|
178
|
+
const { notificationId } = (event as CustomEvent<{ notificationId?: string }>).detail
|
|
179
|
+
openNotificationCenter({ notificationId })
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// Host 已有 notification channel 收到原始事件后,同时投递给两个投影。
|
|
183
|
+
function applyNotificationRealtimeEvent(rawEvent: unknown) {
|
|
184
|
+
popover.applyRealtimeEvent(rawEvent)
|
|
185
|
+
center.applyRealtimeEvent(rawEvent)
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`applicationName` 传入非空应用标识时,只加载该应用的消息;显式传入空字符串 `''` 时,表示加载当前用户、当前组织下所有应用的消息。UniplatBase transport 在全应用模式下不发送 `application_name`,不会把空字符串或推断的应用名写入请求。该模式需要 UniplatBase 实现全应用聚合契约;旧后端会回退到 JWT 当前应用,不能达到全量效果。
|
|
190
|
+
|
|
191
|
+
小浮层初始关闭,由 Host 的铃铛调用 `open()`、`close()` 或 `toggle()`。它只查询未读投影,固定展示最近 4 条未读;单条点击先异步标记已读,再关闭浮层并派发 `notification-center-requested`(仅含 `notificationId`);底部“查看全部消息”派发同一事件但 detail 为空。“全部已读”直接复用 transport 的 `markAllRead()`。未读数通过 `unread-count-change` 提供给 Host 渲染铃铛角标。Host 负责浮层锚点位置和打开完整通知页,组件负责原型面板、空态和安全错误提示。组件打开时会在下一宏任务于所属 `document` 注册外部点击监听,不会把调用 `open()/toggle()` 的当前铃铛点击误判为外部点击;因此即使 Host 存在 `overflow`、`transform` 或独立层叠上下文,后续点击组件外仍会关闭。关闭或移除组件后立即解除监听并取消待执行任务。
|
|
192
|
+
|
|
193
|
+
两个组件都不接收 JWT,不建立实时连接。Host 继续维护唯一 `/api/user-realtime/ws` 并订阅 `notification` channel;JWT 只存在于 transport 的 HTTP `Authorization: Bearer` header。小浮层使用原型指定的 Tabler Icons 2.47.0 原始 SVG 路径,不依赖 Host 图标字体。可通过 `typeIcons` 覆盖后端分类 code 的图标,但不得传入携带认证 query、fragment 或用户凭据的 URL。
|
|
194
|
+
|
|
195
|
+
### User Login
|
|
196
|
+
|
|
197
|
+
`<uniplat-user-login>` 严格按亲亲企服 40.1 注册登录原型重构为 400px 登录卡片,顶部固定提供“密码登录 / 验证码登录 / 扫码登录”三个页签。验证码登录只有手机号和短信验证码,不显示或请求图形验证码;它同时承担新手机号注册入口。字段失焦后就地显示原型错误态,服务端返回稳定的验证码错误码时,错误提示贴在短信字段下方。新用户注册成功后显示原型 1.3 过渡页,2 秒后自动请求 Host 进入入驻引导,也可点击“立即开始”。扫码的待扫码、已扫码和失效状态复用同一个二维码区域,不改变卡片布局。组件不依赖 Vue 或 Ant Design,所有样式位于 Shadow DOM 中。
|
|
198
|
+
|
|
199
|
+
对接现有 UniplatBase 登录接口时使用专用 transport。`apiBaseUrl` 是 Gateway 基础地址;`setAccessToken()` 由宿主负责原子保存登录 JWT,并可使用第二个参数中的无凭据登录元数据初始化用户和组织状态:
|
|
200
|
+
|
|
201
|
+
```html
|
|
202
|
+
<uniplat-user-login id="user-login"></uniplat-user-login>
|
|
203
|
+
|
|
204
|
+
<script type="module">
|
|
205
|
+
import { createUniplatBaseUserLoginTransport } from '@uniplat/ai-employee-web-components'
|
|
206
|
+
|
|
207
|
+
const transport = createUniplatBaseUserLoginTransport({
|
|
208
|
+
apiBaseUrl: '/api',
|
|
209
|
+
async setAccessToken(nextToken, metadata) {
|
|
210
|
+
await authStore.completeLogin(nextToken, metadata)
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
document.querySelector('#user-login').configure({
|
|
215
|
+
transport,
|
|
216
|
+
applicationName: 'HrSaas',
|
|
217
|
+
headerTitle: '登录企业工作台',
|
|
218
|
+
headerSubtitle: '手机号验证后进入企业服务平台',
|
|
219
|
+
headerIconUrl: '/assets/login-shield.svg',
|
|
220
|
+
securityHint: '登录凭证仅用于企业工作台认证,请勿向他人透露验证码',
|
|
221
|
+
userAgreementUrl: '/agreements/user',
|
|
222
|
+
privacyAgreementUrl: '/agreements/privacy',
|
|
223
|
+
forgotPasswordUrl: '/account/forgot-password',
|
|
224
|
+
initialMethod: 'verification-code',
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
document.querySelector('#user-login').addEventListener('onboarding-requested', (event) => {
|
|
228
|
+
document.querySelector('#account-context')?.startOrganizationOnboarding()
|
|
229
|
+
})
|
|
230
|
+
</script>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`applicationName` 由 Host 通过 `configure()` 提供,组件在登录时把它交给 transport,用于请求目标应用身份;它不会进入 DOM。`headerTitle`、`headerSubtitle`、`headerIconUrl` 和 `securityHint` 同样由 Host 提供,组件负责渲染顶部品牌区和底部安全区。它们不应包含 JWT、验证码或其他认证凭据。`headerIconUrl` 仅接受不含认证参数的 HTTP(S) 或图片 Data URL。右上角关闭按钮仍由宿主弹窗负责,因为组件不管理宿主弹窗的打开和关闭状态。
|
|
234
|
+
|
|
235
|
+
`UserLoginTransport` 的短信发送和验证码登录方法是必需能力。短信发送只接收 `{ mobile }`;旧版可选 `createCaptcha()` 为源码兼容保留,但组件不会调用。自定义 Host transport 的密码和扫码能力仍为可选:只有 Host 同时提供对应方法时页签才可点击,未配置的页签保持显示但禁用。`createUniplatBaseUserLoginTransport()` 已对接后端统一应用登录接口并提供三种登录能力,因此直接使用该 factory 时三个页签均可用:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const transport: UserLoginTransport = {
|
|
239
|
+
sendSmsCode,
|
|
240
|
+
login,
|
|
241
|
+
loginWithPassword: hostAuth.loginWithPassword,
|
|
242
|
+
createQrChallenge: hostAuth.createQrChallenge,
|
|
243
|
+
pollQrChallenge: hostAuth.pollQrChallenge,
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
扫码 transport 应在确认成功前完成 JWT 的安全保存,只向组件返回 `{ status: 'confirmed', result: { applicationName } }`。二维码 challenge ID 仅在 transport 与组件内部短暂传递,不进入 DOM、事件或日志;二维码图片只接受图片 Data URL,或不带 query/hash/用户凭据的 HTTP(S) URL。组件优先使用创建响应中的 `pollIntervalMs`,自定义 transport 未提供时才使用 `configure()` 的 `qrPollIntervalMs`。收到确认、拒绝或过期状态,以及切换页签、重新配置或移除时,组件都会停止二维码轮询。
|
|
248
|
+
|
|
249
|
+
专用 transport 使用以下 UniplatBase 接口:
|
|
250
|
+
|
|
251
|
+
```text
|
|
252
|
+
POST /api/general/project/uniplat_base/service/anonymous/system.user/sendVerifyCode
|
|
253
|
+
POST /api/general/project/uniplat_base/service/anonymous/application.identity/login-application
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
短信接口的请求体为 `{ "mobile": "..." }`,不携带图形验证码 seed 或 verifycode。`login-application` 通过 `auth_method=sms|password|qr` 复用相同的应用身份选择与 JWT 响应;专用 transport 自动将 `applicationName` 同时传为 `application_name`、`client_id`,并收集浏览器设备类型、名称和品牌。它在本地根据浏览器与设备特征计算 SHA-256 摘要,作为稳定的 `device_id` 上报;原始指纹特征不会上报。扫码创建和轮询分别使用 `action=create|poll`。调用方无需为此升级配置;如已有合规的稳定设备 ID,可选在 factory 的 `deviceInfo.deviceId` 中覆盖默认值,SDK 不写入 localStorage、Cookie 或其他宿主存储。完整后端契约见 `/Users/gaostudio/.vibe-requirements/UniplatBase_应用登录密码验证码与扫码接口文档_v0_1.md`。登录 JWT 只会从登录响应传给 `setAccessToken()`,不会进入 URL、DOM、日志或公开事件;密码、challenge ID 和后端原始错误同样不会进入这些渠道。`login-success` 返回 `{ applicationName, isNewUser? }`;`onboarding-requested` 返回 `{ applicationName, trigger }`,其中 `trigger` 为 `automatic` 或 `manual`;`sms-sent` 不返回手机号;`agreement-requested` 返回 `{ type: 'user' | 'privacy' }`;`forgot-password-requested` 不返回手机号。Host 收到入驻事件后负责进入原型 02 流程。错误事件只包含白名单错误码与安全提示。
|
|
257
|
+
|
|
258
|
+
组件支持以下 CSS Variables:`--uniplat-user-login-width`、`--uniplat-user-login-primary`、`--uniplat-user-login-text`、`--uniplat-user-login-muted`、`--uniplat-user-login-placeholder`、`--uniplat-user-login-background`、`--uniplat-user-login-input-background`、`--uniplat-user-login-error` 和 `--uniplat-user-login-font-family`。
|
|
259
|
+
|
|
260
|
+
公开 `::part`:`shell`、`header`、`header-icon`、`header-title`、`header-subtitle`、`form`、`title`(兼容别名,现指页签栏)、`tabs`、`tab`、`method-panel`、`field`、`input`、`sms-row`、`send-button`、`password-toggle`、`login-options`、`forgot-password`、`agreement`、`agreement-link`、`submit-button`、`qr-panel`、`qr-code`、`qr-overlay`、`qr-tip`、`registration-success`、`registration-success-icon`、`onboarding-button`、`field-error`、`status`、`error`、`security-footer` 和 `security-icon`。
|
|
261
|
+
|
|
262
|
+
### 内置组织入驻
|
|
263
|
+
|
|
264
|
+
40.1 原型的入驻方式选择、跳过确认、企业/团队创建、校验与成功状态已内置到 `<uniplat-account-context>`。不再提供 `<uniplat-organization-onboarding>` 标签、类或独立 `configure()` 接口;账户菜单和无归属个人态点击“创建团队 / 企业”会直接打开内部流程。Host 只需在账户组件配置中注入创建 transport;邀请加入、企业认证、稍后认证、团队升级和邀请成员仍以业务事件交由 Host 打开对应流程。
|
|
265
|
+
|
|
266
|
+
生产 Host 应注入 `OrganizationOnboardingTransport`:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
interface OrganizationOnboardingTransport {
|
|
270
|
+
createEnterprise(request: {
|
|
271
|
+
organizationName: string
|
|
272
|
+
enterpriseName: string
|
|
273
|
+
unifiedSocialCreditCode: string
|
|
274
|
+
}): Promise<{ id: string; name: string; type: 'enterprise' }>
|
|
275
|
+
createTeam(request: { teamName: string }): Promise<{ id: string; name: string; type: 'team' }>
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
transport 可抛出 `OrganizationOnboardingTransportError`,稳定错误码为 `ENTERPRISE_NAME_CONFLICT`、`CREDIT_CODE_CONFLICT`、`TEAM_NAME_CONFLICT` 和 `UNKNOWN`。原始后端异常不会进入 DOM 或公共事件。
|
|
280
|
+
|
|
281
|
+
UniplatBase 已在现有 `system.org` 服务下提供真实创建能力。Host 使用当前 JWT 对接时,不需要自行拼请求体:
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
import { createUniplatBaseOrganizationOnboardingTransport } from '@uniplat/ai-employee-web-components'
|
|
285
|
+
|
|
286
|
+
accountContext.configure({
|
|
287
|
+
applicationName: 'OfficialWebsite',
|
|
288
|
+
organizationOnboarding: {
|
|
289
|
+
transport: createUniplatBaseOrganizationOnboardingTransport({
|
|
290
|
+
serviceBaseUrl: '/api/general/project/uniplat_base/service',
|
|
291
|
+
async getAccessToken() {
|
|
292
|
+
return authStore.getCurrentAccessToken()
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
},
|
|
296
|
+
})
|
|
297
|
+
```
|
|
298
|
+
|
|
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
|
+
|
|
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 定制该浮层。
|
|
302
|
+
|
|
303
|
+
### Organization Selector
|
|
304
|
+
|
|
305
|
+
`<uniplat-organization-selector>` 实现 40.1 原型中登录后存在多个归属时的选择卡片。Host 传入完整组织列表、默认项及“记住选择”的初始值;组件只在用户点击“进入”后派发选择,不自行修改 Host 登录态。
|
|
306
|
+
|
|
307
|
+
```js
|
|
308
|
+
const selector = document.querySelector('uniplat-organization-selector')
|
|
309
|
+
selector.configure({
|
|
310
|
+
selectedOrganizationId: 'enterprise-beijing',
|
|
311
|
+
rememberSelection: true,
|
|
312
|
+
organizations: [
|
|
313
|
+
{
|
|
314
|
+
id: 'enterprise-beijing',
|
|
315
|
+
name: '北京星河科技有限公司',
|
|
316
|
+
type: 'enterprise',
|
|
317
|
+
avatarText: '星',
|
|
318
|
+
verificationStatus: 'verified',
|
|
319
|
+
role: '超管',
|
|
320
|
+
memberCount: 128,
|
|
321
|
+
lastEnteredLabel: '上次进入 2 小时前',
|
|
322
|
+
},
|
|
323
|
+
{ id: 'team-growth', name: '星河增长项目组', type: 'team', role: '成员' },
|
|
324
|
+
],
|
|
325
|
+
})
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
组件派发 `organization-enter-requested`(`{ organizationId, rememberSelection }`)和 `organization-create-requested`(`{}`)。列表超过固定高度时仅列表区滚动,所有归属仍完整渲染。
|
|
329
|
+
|
|
330
|
+
UniplatBase 的组织投影由 `application.identity/list_available_organizations` 返回。Host 应使用 `team_name` 作为选择器展示名,将 `organization_type` 原样映射为 `enterprise | team`,将企业的 `verification_status` 映射为 `unverified | pending | rejected | verified | expired`,并把 `role/member_count/last_active_at` 转为对应展示字段;团队的 `verification_status=not_applicable` 不传给组件。角色显示文案和最近进入时间仍由 Host 本地化,不应由组件解析后端文本。
|
|
331
|
+
|
|
332
|
+
### Organization Multi Employee Selector
|
|
333
|
+
|
|
334
|
+
`<uniplat-organization-multi-employee-selector>` 是当前组织员工多选弹层。组件根据当前 JWT 读取组织架构与在职员工,负责搜索、分页、初始回显、去重、选择上限、受保护成员和最终选择事件;它不判断管理员权限,也不调用管理员或业务角色写入接口。完整接入契约见 [组织员工多选组件接入说明](docs/组织员工多选组件接入说明.md)。
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import {
|
|
338
|
+
createUniplatBaseOrganizationMultiEmployeeSelectorTransport,
|
|
339
|
+
type UniplatOrganizationMultiEmployeeSelectorElement,
|
|
340
|
+
} from '@uniplat/ai-employee-web-components'
|
|
341
|
+
|
|
342
|
+
const selector = document.querySelector<UniplatOrganizationMultiEmployeeSelectorElement>(
|
|
343
|
+
'uniplat-organization-multi-employee-selector',
|
|
344
|
+
)!
|
|
345
|
+
|
|
346
|
+
selector.configure({
|
|
347
|
+
applicationName: 'OfficialWebsite',
|
|
348
|
+
transport: createUniplatBaseOrganizationMultiEmployeeSelectorTransport({
|
|
349
|
+
serviceBaseUrl: '/api/general/project/uniplat_base/service',
|
|
350
|
+
getAccessToken: () => authStore.getCurrentAccessToken(),
|
|
351
|
+
}),
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
await selector.open({
|
|
355
|
+
title: '选择组织管理员',
|
|
356
|
+
initialSelectedEmployeeIds: currentAdministrators.map(({ employeeId }) => employeeId),
|
|
357
|
+
disabledSelections: owner
|
|
358
|
+
? [{ employeeId: owner.employeeId, reason: '组织所有者不能在此处移除' }]
|
|
359
|
+
: [],
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
selector.addEventListener('selection-confirmed', (event) => {
|
|
363
|
+
const { employeeIds } = event.detail
|
|
364
|
+
void organizationApi.setAdministrators(employeeIds, expectedVersion)
|
|
365
|
+
})
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
专用 transport 复用 `system.org_directory/tree_full`、`node_contacts`、`contact_search` 与 `contact_get`,每次请求重新读取 JWT,且 JWT 只进入 `Authorization` header。组织身份完全由 JWT 决定,组件和 Host 都不得提交 `oid/xid/eid/uid/poid` 等身份字段。v0.1 固定为员工多选且只选择在职员工;部门仅用于导航,尚不开放部门批量勾选。
|
|
369
|
+
|
|
370
|
+
### Organization Single Employee Selector
|
|
371
|
+
|
|
372
|
+
`<uniplat-organization-single-employee-selector>` 提供与多选组件一致的组织架构、搜索、分页和在职员工过滤,但公开语义固定为单选。它使用独立标签、单个初始 ID 和标量确认结果,不接受 `selectionMode`、`maxSelection` 或多选 ID 数组。完整契约见 [组织员工单选组件接入说明](docs/组织员工单选组件接入说明.md)。
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
const selector = document.querySelector('uniplat-organization-single-employee-selector')
|
|
376
|
+
selector.configure({
|
|
377
|
+
applicationName: 'OfficialWebsite',
|
|
378
|
+
transport: createUniplatBaseOrganizationSingleEmployeeSelectorTransport({
|
|
379
|
+
serviceBaseUrl: '/api/general/project/uniplat_base/service',
|
|
380
|
+
getAccessToken: () => authStore.getCurrentAccessToken(),
|
|
381
|
+
}),
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
await selector.open({
|
|
385
|
+
title: '选择组织负责人',
|
|
386
|
+
initialSelectedEmployeeId: currentOwnerEmployeeId,
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
selector.addEventListener('selection-confirmed', (event) => {
|
|
390
|
+
void organizationApi.setOwner(event.detail.employeeId)
|
|
391
|
+
})
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Account Context
|
|
395
|
+
|
|
396
|
+
`<uniplat-account-context>` 是 40.1 原型导航右侧的统一账户工作台组件,不包含品牌、全局导航菜单或整条导航。它在一个 Host 元素内组合工作台入口、当前账户胶囊、应用入口浮层以及企业/团队切换浮层;两个浮层互斥且绝对定位,不改变导航布局。
|
|
397
|
+
|
|
398
|
+
账户区和组织菜单中的工作台、选择器、认证状态、选中、创建及退出图标使用 40.1 原型指定的 Tabler Icons 2.47.0 原始 SVG 路径;组织列表左侧使用原型中的企业建筑或团队成员图标。组件将这些原型资源打入独立运行时,不依赖 Host 的图标字体或全局样式。
|
|
399
|
+
|
|
400
|
+
```js
|
|
401
|
+
import {
|
|
402
|
+
createUniplatBaseAccountContextTransport,
|
|
403
|
+
createUniplatBaseApplicationSwitcherTransport,
|
|
404
|
+
createUniplatBaseOrganizationOnboardingTransport,
|
|
405
|
+
} from '@uniplat/ai-employee-web-components'
|
|
406
|
+
|
|
407
|
+
const applicationName = 'OfficialWebsite'
|
|
408
|
+
const accountContext = document.querySelector('uniplat-account-context')
|
|
409
|
+
const accountTransport = createUniplatBaseAccountContextTransport({
|
|
410
|
+
serviceBaseUrl,
|
|
411
|
+
getAccessToken: () => authStore.getAccessToken(),
|
|
412
|
+
setAccessToken: (nextToken) => authStore.setAccessToken(nextToken),
|
|
413
|
+
})
|
|
414
|
+
const workbenchTransport = createUniplatBaseApplicationSwitcherTransport({ serviceBaseUrl })
|
|
415
|
+
|
|
416
|
+
accountContext.configure({
|
|
417
|
+
applicationName,
|
|
418
|
+
transport: accountTransport,
|
|
419
|
+
workbench: {
|
|
420
|
+
getAccessToken: () => authStore.getAccessToken(),
|
|
421
|
+
transport: workbenchTransport,
|
|
422
|
+
},
|
|
423
|
+
organizationOnboarding: {
|
|
424
|
+
transport: createUniplatBaseOrganizationOnboardingTransport({
|
|
425
|
+
serviceBaseUrl,
|
|
426
|
+
getAccessToken: () => authStore.getAccessToken(),
|
|
427
|
+
}),
|
|
428
|
+
},
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
accountContext.addEventListener('organization-selected', async (event) => {
|
|
432
|
+
await accountContext.switchOrganization(event.detail.organizationId)
|
|
433
|
+
})
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
有当前组织时默认显示“工作台”按钮和带组织数量的账户胶囊;账户胶囊及展开菜单资料区优先显示服务端当前组织投影的员工姓名与头像。未选组织或当前组织没有员工身份时,回退到 `list_available_organizations` 顶层返回的用户姓名与头像。组织列表左侧严格使用原型中的企业建筑或团队成员图标。企业认证状态收进组织名后的紧凑图标,团队显示“无需认证”的类型状态,认证未通过或过期时账户胶囊出现红点。服务端返回无当前组织时显示“创建团队 / 企业”和个人账户。`showWorkbench`、`showVerificationStatus` 仍可显式关闭对应视觉能力。
|
|
437
|
+
|
|
438
|
+
`applicationName` 是 Host 当前应用的稳定标识,必须显式传入。Host 不再提供姓名、头像、当前组织、可选组织或企业认证状态;组件通过账户 transport 自行调用 `list_available_applications` 与 `list_available_organizations`,后者同时提供用户的 `display_name`、`avatar_url` 与 `mobile_masked`,按服务端投影完整渲染,并可用 `refresh()` 主动刷新。应用接口只提供当前身份和可用应用;组织接口以 `{ application_name }` 返回该应用可进入的组织和用户资料,两者不得互相替代。选择组织时组件仍先派发 `organization-selected`,由 Host 决定是否调用 `accountContext.switchOrganization(id)`;方法内部完成 JWT 切换、Host `setAccessToken()` 回调和组件重绘,Host 不解析响应或重新配置展示数据。Open Application 切换请求使用服务端 `xid + organization_oid`。当前后端对组织型应用切换仍要求目标组织对应的 `xid`,但新的两个列表接口不返回该值;因此 SDK 会拒绝组织型应用切换到非当前组织,直到服务端提供可安全解析目标实例的既有切换能力。调用方不能提交身份字段。组件不直接接收或保存 JWT,专用 transport 每次从 Host 的 `getAccessToken()` 获取。旧的 `displayName/organizations` 等手工展示配置仅为兼容保留,不再推荐。
|
|
439
|
+
|
|
440
|
+
使用 UniplatBase 专用账户 transport 时,账户菜单的“个人账号设置”会由组件直接打开新窗口:它会先把 `/personal` 按当前页面解析为完整 HTTP(S) URL,再以当前 `applicationName`(官网为 `OfficialWebsite`)调用 `switch_current_application`,传入 `{ application_name: applicationName, response_type: 'ticket', entry_url }`,并对同一个完整入口 URL 追加 `#uniplat_application_ticket=<ticket>` 后跳转。这样目标页兑换到的是当前应用的 JWT;ticket 只在局部导航 URL 中短暂存在。独立 `<uniplat-user-menu>` 仍只派发 `account-settings-requested` 事件。
|
|
441
|
+
|
|
442
|
+
账户菜单内点击“我的员工信息”会直接打开组件内的原型弹窗,不需要 Host 再配置页面或手工拼装资料。所属企业、姓名和角色来自当前组织投影;弹窗打开后才通过账户 transport 按当前 JWT 请求员工主部门,避免给高频组织列表增加字段和查询成本。专用 UniplatBase transport 使用 `system.employee_profile/get_current_employee_info`;部门缺失或请求失败时显示“未设置”,状态按当前有效员工身份显示“在职”。`employee-info-requested` 事件仍会冒泡,供既有调用方做统计或附加业务,但不再需要用它打开该弹窗。
|
|
443
|
+
|
|
444
|
+
账户菜单底部和个人态的“创建企业 / 团队”会直接在账户组件内启动原型 02 流程,不会派发 `organization-create-requested` 或 `create-organization-requested`。如需从登录成功后进入入驻流程,Host 调用 `accountContext.startOrganizationOnboarding()`。
|
|
445
|
+
|
|
446
|
+
`workbench` 是内嵌 Application Switcher 的独立能力配置:其中 `getAccessToken` 为应用入口请求提供当前 JWT,`transport` 负责读取可见应用并在点击时签发短期 ticket。它只服务“工作台”九宫格,不参与账户姓名、头像、组织列表或认证状态的加载。统一账户区不会在工作台浮层中重复显示组织选择器。工作台应用按固定九宫格、新窗口和短期 ticket 规则运行,并优先以 `application_scope=open` 判断开放应用;仅在旧响应缺少该字段时临时回退 `oid=0`。组件派发账户、组织切换、工作台、退出和内部入驻流程所需的净化事件;公共事件不包含应用名、显示名、手机号、组织名称、头像 URL、JWT 或 ticket。
|
|
447
|
+
|
|
448
|
+
专用 transport 对外只抛出 `AUTH_REQUIRED`、`INVALID_RESPONSE`、`ORGANIZATION_UNAVAILABLE`、`ORGANIZATION_SWITCH_UNSUPPORTED`、`REQUEST_FAILED` 和 `TOKEN_PERSIST_FAILED` 六类稳定错误码,不透传服务端消息或原始响应。切换成功响应中的 JWT 会先交给 Host `setAccessToken()`,返回给调用方的 projection 不含 JWT。
|
|
449
|
+
|
|
450
|
+
正常情况下 `employee_avatar_url` 应由服务端返回可匿名读取的安全 HTTP(S) URL。为兼容已部署服务曾返回的员工头像受管存储路径,专用 transport 只对严格匹配 `<xid>/.root/.avatars/<eid>/avatar-<uuid>.<ext>` 且 `eid` 与组织投影一致的值,转换为同一 `serviceBaseUrl` 下的匿名头像接口;内部存储路径不会进入组件 DOM。其他非 URL 值或身份不一致的路径按无效响应拒绝。
|
|
451
|
+
|
|
452
|
+
账户区公开 `::part`:`context`、`create-action`、`workbench-action`、`account-trigger`、`avatar`、`avatar-image`、`display-name`、`organization-name`、`verification-status`、`organization-count`、`workbench-panel`、`account-panel`、`onboarding-panel`、`employee-info-modal`、`employee-info-dialog`、`employee-info-close` 和 `employee-info-details`。
|
|
453
|
+
|
|
454
|
+
### User Menu
|
|
455
|
+
|
|
456
|
+
`<uniplat-user-menu>` 作为兼容入口保留,并与统一账户区内部的 40.1 组织切换菜单使用同一实现。它展示头像、显示名、脱敏手机号、“个人账号设置”、随当前组织变化的“我的员工信息”、带数量的企业/团队混合列表、企业认证状态、创建入口和退出登录。新接入应优先只使用 `<uniplat-account-context>`。
|
|
457
|
+
|
|
458
|
+
组件不请求身份接口,也不接收 JWT。Host 从自身登录态或已有身份响应中提取显示数据后,通过 JavaScript property 配置:
|
|
459
|
+
|
|
460
|
+
```html
|
|
461
|
+
<uniplat-user-menu id="user-menu"></uniplat-user-menu>
|
|
462
|
+
|
|
463
|
+
<script type="module">
|
|
464
|
+
import '@uniplat/ai-employee-web-components'
|
|
465
|
+
|
|
466
|
+
const userMenu = document.querySelector('#user-menu')
|
|
467
|
+
userMenu.configure({
|
|
468
|
+
displayName: '张伟',
|
|
469
|
+
maskedMobile: '138****6688',
|
|
470
|
+
avatarUrl: 'https://static.example.com/employees/zhang-wei.png',
|
|
471
|
+
currentOrganizationId: 'company-beijing',
|
|
472
|
+
organizations: [
|
|
473
|
+
{
|
|
474
|
+
id: 'company-beijing',
|
|
475
|
+
name: '北京星河科技有限公司',
|
|
476
|
+
type: 'company',
|
|
477
|
+
avatarText: '星',
|
|
478
|
+
role: '超管',
|
|
479
|
+
memberCount: 128,
|
|
480
|
+
verificationStatus: 'verified',
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
id: 'team-growth',
|
|
484
|
+
name: '星河增长项目组',
|
|
485
|
+
type: 'team',
|
|
486
|
+
role: '团队负责人',
|
|
487
|
+
memberCount: 9,
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
userMenu.addEventListener('organization-selected', (event) => {
|
|
493
|
+
// Host 调用自己的组织切换流程,成功后重新 configure() 并关闭浮层。
|
|
494
|
+
switchOrganization(event.detail.organizationId)
|
|
495
|
+
})
|
|
496
|
+
</script>
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
`organizations` 只用于展示和选择,不会被组件提交给任何服务。`maskedMobile` 只能传脱敏值,组件会拒绝连续 11 位的中国大陆手机号。组织点击是受控交互:组件派发事件但不自行改变选中项,Host 应在切换成功后使用新的 `currentOrganizationId` 重新配置。
|
|
500
|
+
|
|
501
|
+
组件派发以下 `bubbles: true`、`composed: true` 事件:
|
|
502
|
+
|
|
503
|
+
```text
|
|
504
|
+
account-settings-requested detail: {}
|
|
505
|
+
employee-info-requested detail: { organizationId | null }
|
|
506
|
+
organization-selected detail: { organizationId }
|
|
507
|
+
organization-create-requested detail: {}
|
|
508
|
+
logout-requested detail: {}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
事件不会包含显示名、手机号、组织名称、JWT 或内部配置。组件支持 CSS Variables:`--uniplat-user-menu-width`、`--uniplat-user-menu-primary`、`--uniplat-user-menu-text`、`--uniplat-user-menu-muted`、`--uniplat-user-menu-border`、`--uniplat-user-menu-surface`、`--uniplat-user-menu-hover`、`--uniplat-user-menu-selected`、`--uniplat-user-menu-list-max-height` 和 `--uniplat-user-menu-font-family`。
|
|
512
|
+
|
|
513
|
+
公开 `::part`:`panel`、`profile`、`avatar`、`avatar-image`、`profile-name`、`profile-mobile`、`menu-actions`、`account-settings-action`、`employee-info-action`、`section-title`、`organization-list`、`organization`、`organization-icon`、`organization-name`、`organization-type`、`verification-status`、`organization-meta`、`organization-check`、`empty`、`footer`、`create-footer`、`logout-footer`、`create-action` 和 `logout-action`。
|
|
514
|
+
|
|
515
|
+
### 工作台内置应用切换
|
|
516
|
+
|
|
517
|
+
不再提供或注册 `<uniplat-application-switcher>`。工作台应用面板仅作为 `<uniplat-account-context>` 的内部能力,通过其 `workbench` 配置启用;Host 不得创建、配置或监听独立的应用切换元素。内嵌面板保留固定 3 列网格、新窗口和短期 ticket 跳转。
|
|
518
|
+
|
|
519
|
+
> 破坏性变更(v0.8.0):独立应用切换标签、其类和标签常量已移除。调用方应将原 `applicationSwitcher.configure(...)` 迁移至 `accountContext.configure({ workbench: ... })`;未迁移的页面不会注册旧标签,原有独立工作台入口将不再显示。
|
|
520
|
+
|
|
521
|
+
对接 UniplatBase 时使用专用 transport。Application Switcher Host 只提供当前 JWT,不保存目标应用 JWT:
|
|
522
|
+
|
|
523
|
+
```js
|
|
524
|
+
import { createUniplatBaseApplicationSwitcherTransport } from '@uniplat/ai-employee-web-components'
|
|
525
|
+
|
|
526
|
+
const transport = createUniplatBaseApplicationSwitcherTransport({
|
|
527
|
+
serviceBaseUrl: '/api/general/project/uniplat_base/service',
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
accountContext.configure({
|
|
531
|
+
applicationName: 'OfficialWebsite',
|
|
532
|
+
transport: accountTransport,
|
|
533
|
+
workbench: { getAccessToken: () => authStore.getAccessToken(), transport },
|
|
534
|
+
})
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
内嵌工作台使用当前 JWT 以 `POST {}` 请求 `application.identity/list_available_applications`,其中 `available_applications` 是应用网格唯一数据源。内嵌面板不提供组织选择器,因此不会请求 `list_available_organizations`;组织上下文由 Account Context 自身管理。响应中当前组织或 `application_scope=open` 的应用会按原顺序显示,不截断为前 9 个。
|
|
538
|
+
|
|
539
|
+
点击应用时,transport 会重新读取当前 JWT,以 `POST {}` 调用 `application.identity/issue_ticket`。本地和外部入口都直接使用 `list_available_applications` 当前选中项的 `entry_url`,并追加 `#uniplat_identity_ticket=<ticket>&source_system=<source_system>`;`source_system` 必须取自签发响应,不能用列表中的目标系统代替。Fragment 不携带目标应用或 `client_id`,当前 Host JWT 不会被替换,Ticket 也不会进入公开事件或持久状态。已有 `launch_target` 字段继续用于列表兼容,但不进入新 Ticket 签发请求。
|
|
540
|
+
|
|
541
|
+
`application-selected` 是 Account Context 冒泡的可取消事件。事件只包含原始安全入口,不包含 ticket;宿主阻止默认导航时,预开的空白窗口会关闭,已签发 ticket 等待服务端自动过期:
|
|
542
|
+
|
|
543
|
+
```js
|
|
544
|
+
accountContext.addEventListener('application-selected', (event) => {
|
|
545
|
+
if (!hostPolicy.allowsApplication(event.detail.entry.id)) event.preventDefault()
|
|
546
|
+
})
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
目标应用需要在启动最早阶段登录 Ticket。目标应用只能取自该前端项目的初始化配置,不能由 URL 覆盖。helper 一旦发现 Ticket,会先从地址栏删除 `uniplat_identity_ticket` 与 `source_system`、保留其他 Fragment 参数,再请求登录接口,并只把校验后的 JWT 交给目标 Host:
|
|
550
|
+
|
|
551
|
+
```js
|
|
552
|
+
import { loginWithUniplatBaseIdentityTicket } from '@uniplat/ai-employee-web-components'
|
|
553
|
+
|
|
554
|
+
await loginWithUniplatBaseIdentityTicket({
|
|
555
|
+
serviceBaseUrl: '/api/general/project/uniplat_base/service',
|
|
556
|
+
applicationName: 'HrSaas',
|
|
557
|
+
async setAccessToken(nextToken) {
|
|
558
|
+
await authStore.setAccessToken(nextToken)
|
|
559
|
+
},
|
|
560
|
+
})
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
没有 Ticket 时 helper 返回 `false`,不会请求服务器;成功登录并完成 `setAccessToken()` 后返回 `true`。请求固定使用初始化的 `applicationName`,可将 Fragment 中非空的 `source_system` 透明传给 `anonymous/application.identity/login_with_ticket`,但不传 `client_id`。响应中的 `application_name` 必须与初始化配置完全一致,否则不会保存 JWT。JWT 与短期 Ticket 均不得进入 DOM、公开事件、持久状态、日志或错误消息。
|
|
564
|
+
|
|
565
|
+
旧的 `exchangeUniplatBaseApplicationTicket()`、`uniplat_application_ticket` Fragment 和后端 `exchange_application_ticket` 协议继续保留,供尚未迁移的显式调用方使用;Application Switcher 默认流程不再调用它们。
|
|
566
|
+
|
|
567
|
+
对接其他、已经提供扁平 Application Switcher 接口的服务时,可以使用通用 HTTP transport:
|
|
568
|
+
|
|
569
|
+
```js
|
|
570
|
+
import { createHttpApplicationSwitcherTransport } from '@uniplat/ai-employee-web-components'
|
|
571
|
+
|
|
572
|
+
const transport = createHttpApplicationSwitcherTransport({
|
|
573
|
+
endpoint: 'https://hr-saas.example.com/api/application-entries',
|
|
574
|
+
organizationsEndpoint: 'https://hr-saas.example.com/api/organizations',
|
|
575
|
+
switchOrganizationEndpoint: 'https://hr-saas.example.com/api/current-organization',
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
accountContext.configure({
|
|
579
|
+
applicationName: 'OfficialWebsite',
|
|
580
|
+
transport: accountTransport,
|
|
581
|
+
workbench: { getAccessToken: () => authStore.getAccessToken(), transport },
|
|
582
|
+
})
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
通用 HTTP transport 在内嵌工作台中只以 `GET` 请求应用入口,并只在 `Authorization: Bearer <JWT>` 请求头中携带令牌。应用入口响应格式:
|
|
586
|
+
|
|
587
|
+
```json
|
|
588
|
+
{
|
|
589
|
+
"entries": [
|
|
590
|
+
{
|
|
591
|
+
"id": "enterprise-admin",
|
|
592
|
+
"name": "企业管理",
|
|
593
|
+
"url": "https://apps.example.com/enterprise-admin",
|
|
594
|
+
"iconUrl": "https://apps.example.com/icons/enterprise-admin.svg",
|
|
595
|
+
"description": "企业管理后台",
|
|
596
|
+
"openMode": "blank"
|
|
597
|
+
}
|
|
598
|
+
]
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
应用入口的 `id`、`name`、`url` 必填;`iconUrl` 和 `description` 可选,所有应用统一在新窗口打开。需要切换应用身份时,内嵌工作台会在用户点击时先打开不含凭据的空白窗口,等待短期 Ticket 签发完成后再导航,避免覆盖 Host 当前页面。组件加载时及调用 `refresh()` 时重新取得 JWT。组织切换只由 Account Context 处理。UniplatBase 专用 transport 返回当前组织级应用和所有 `application_scope=open` 应用;只有旧响应缺少范围字段时才以 `oid=0` 兼容判断。当前工作台组织上下文来自 Account Context;`issue_ticket` 始终按点击时读取到的当前 JWT 签发,不接受目标应用、实例、组织或入口参数。工作台内部派发的事件不会暴露到公开组件契约,且不包含 JWT 或 Ticket。
|
|
603
|
+
|
|
604
|
+
### AI Employee Chat
|
|
605
|
+
|
|
606
|
+
```html
|
|
607
|
+
<uniplat-ai-employee-chat id="chat"></uniplat-ai-employee-chat>
|
|
608
|
+
|
|
609
|
+
<script type="module">
|
|
610
|
+
import '@uniplat/ai-employee-web-components'
|
|
611
|
+
|
|
612
|
+
const chat = document.querySelector('#chat')
|
|
613
|
+
chat.configure({
|
|
614
|
+
realtimeUrl: 'wss://kanban.example.com/api/user-realtime/ws',
|
|
615
|
+
aeCode: 'recruit',
|
|
616
|
+
agentCode: 'default',
|
|
617
|
+
transport,
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
document.querySelector('#open-chat').addEventListener('click', () => chat.open())
|
|
621
|
+
</script>
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
组件初始关闭;Host 只负责配置并在需要时调用 `open()`。`chat.open(initialMessage?)` 可接收一个可选字符串:传入非空字符串时,组件打开并通过既有发送链路自动发送该内容;未配置 `sessionId` 时原子创建会话并提交首条消息,已配置 `sessionId` 时作为已有会话的后续消息发送。未传、空字符串或纯空白时仍只打开抽屉,保持原有交互。组件自身负责 40 号官网原型中的固定右侧抽屉、标题、关闭按钮、消息区、输入区和“转人工”入口;右上角关闭按钮和 Escape 会调用组件自身的 `close()` 并释放连接。Host 不应再包裹另一层抽屉、标题、介绍卡片或安全提示。
|
|
625
|
+
|
|
626
|
+
数字员工的 `assistant` 回复按 Markdown 渲染,支持标题、强调、删除线、行内代码、代码块、引用、有序/无序列表、分隔线及安全链接;用户消息、系统消息和错误消息仍按普通文本显示。原始 HTML 不会注入 DOM,非 HTTP(S) 或 `mailto:` 链接不会生成可点击链接。
|
|
627
|
+
|
|
628
|
+
点击“转人工”后,数字员工连接会先释放,再由同一抽屉切换为 `<uniplat-customer-support-chat>`;不会向数字员工发送一条模拟转人工文本,也不要求 Host 创建第二个抽屉。默认情况下,组件复用数字员工的 `realtimeUrl` 和 `transport.getAccessToken()`,并请求同源 `/api/general/project/hr_saas/service/customer_support_api`,因此使用标准网关路径的 Host 无需增加配置。只有客服网关地址或鉴权提供者不同时,才显式传入 `customerSupport` 覆盖默认配置:
|
|
629
|
+
|
|
630
|
+
```ts
|
|
631
|
+
import { createHrSaasCustomerSupportTransport } from '@uniplat/ai-employee-web-components'
|
|
632
|
+
|
|
633
|
+
const customerSupportTransport = createHrSaasCustomerSupportTransport({
|
|
634
|
+
serviceBaseUrl: '/api/general/project/hr_saas/service/customer_support_api',
|
|
635
|
+
getAccessToken: () => auth.getCurrentAccessToken(),
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
chat.configure({
|
|
639
|
+
realtimeUrl: 'wss://kanban.example.com/api/user-realtime/ws',
|
|
640
|
+
aeCode: 'recruit',
|
|
641
|
+
agentCode: 'default',
|
|
642
|
+
transport,
|
|
643
|
+
customerSupport: {
|
|
644
|
+
realtimeUrl: 'wss://kanban.example.com/api/user-realtime/ws',
|
|
645
|
+
transport: customerSupportTransport,
|
|
646
|
+
},
|
|
647
|
+
})
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### Customer Support Chat
|
|
651
|
+
|
|
652
|
+
`<uniplat-customer-support-chat>` 也可以独立使用。它直接复用 HR-SaaS 的 `user_session_page`、`user_session_create`、`user_message_page`、`user_message_send` 和 `user_mark_read`,实时连接订阅 Kanban 的 `customer_support` channel。首条消息通过 `user_session_create.initial_message` 原子创建会话,后续消息使用 `user_message_send`。打开抽屉或点击“新对话”只进入本地空白态,不提前创建等待中会话;历史遗留的无消息 `waiting` 会话也不会被自动恢复。只有首条消息成功创建会话后,界面才显示“等待客服接入”。
|
|
653
|
+
|
|
654
|
+
```html
|
|
655
|
+
<uniplat-customer-support-chat id="customer-support"></uniplat-customer-support-chat>
|
|
656
|
+
<script type="module">
|
|
657
|
+
import { createHrSaasCustomerSupportTransport } from '@uniplat/ai-employee-web-components'
|
|
658
|
+
|
|
659
|
+
const customerSupport = document.querySelector('#customer-support')
|
|
660
|
+
customerSupport.configure({
|
|
661
|
+
realtimeUrl: 'wss://kanban.example.com/api/user-realtime/ws',
|
|
662
|
+
transport: createHrSaasCustomerSupportTransport({
|
|
663
|
+
serviceBaseUrl: '/api/general/project/hr_saas/service/customer_support_api',
|
|
664
|
+
getAccessToken: () => auth.getCurrentAccessToken(),
|
|
665
|
+
}),
|
|
666
|
+
})
|
|
667
|
+
await customerSupport.open()
|
|
668
|
+
</script>
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
组件派发 `ready`、`message-sent`、`message-received`、`connection-state-change`、`ai-chat-requested`、`closed` 和 `error`。点击左下角“转AI客服”时派发不含 detail 数据的 `ai-chat-requested`;当组件嵌入 `<uniplat-ai-employee-chat>` 时,由父组件内部接管该事件、释放人工客服连接并恢复 AI 客服,无需 Host 处理。独立使用时,Host 可监听该事件切换自己的界面。JWT 每次请求时重新取得,只进入 HTTP `Authorization` header 和 WebSocket `authenticate` 首帧;不会进入 URL、DOM、事件或日志。
|
|
672
|
+
|
|
673
|
+
人工客服标题下方固定用于显示当前会话状态:尚未建立、等待接入、人工客服已接入或会话已结束。其下方的独立状态栏再结合实时连接状态,显示客服通道连接、实时连接恢复、等待接入、人工客服在线或会话结束,不依赖额外的后端在线字段。
|
|
674
|
+
|
|
675
|
+
客服客户端按会话维护已确认、提交中和待提交的已读游标。重复或回退的游标不会再次调用 `markRead()`;提交期间只保留更大的待提交游标,失败后仅在后续真实消息刷新或通道恢复时重试。实时事件按语义处理:`support_message_created` 刷新消息历史,`support_read_state_changed` 不触发历史刷新,会话、成员和持麦等状态事件只刷新会话投影,`channel_resync_required` 才执行完整恢复,从而避免“已读回执 → 历史刷新 → 再次已读”的反馈环。
|
|
676
|
+
|
|
677
|
+
标题左侧的会话按钮会切换到原型“我的会话”视图。Host 可在现有 transport 上实现可选 `listSessions()`,返回 `sessionId`、标题、最后消息摘要、更新时间、未读数和可选最后消息 ID;用户点击列表项后,组件自行调用 core `bindSession()` 加载历史并切换实时订阅。可选 `markSessionRead(sessionId, latestMessageId)` 用于复用 Host 已有的已读接口。当前选定会话始终按已读处理,在聊天界面和“我的会话”列表中都不显示未读角标,也不计入标题入口的聚合未读数;历史加载、列表加载、当前会话的新实时回复以及从会话列表返回都会立即清除当前会话本地角标并异步同步 Host。历史消息 ID 与列表 `latestMessageId` 不一致时,组件会用列表返回的 ID 再次同步;同步失败不阻断聊天,并会在下一次触发时重试。其他未选定会话的新回复仍保留未读。旧 transport 未实现会话列表,或真实列表暂时加载失败时,聊天和发送能力不受影响,面板至少显示组件已绑定的当前会话。
|
|
678
|
+
|
|
679
|
+
组件派发可冒泡、可穿过 Shadow DOM 的 `ready`、`message-sent`、`message-received`、`connection-state-change` 和 `error` 事件。事件只包含会话、消息、状态或经过净化的错误信息,不包含认证凭据。
|
|
680
|
+
|
|
681
|
+
可用的样式变量包括:
|
|
682
|
+
|
|
683
|
+
```css
|
|
684
|
+
uniplat-ai-employee-chat {
|
|
685
|
+
--uniplat-chat-primary: #2e6bf5;
|
|
686
|
+
--uniplat-chat-primary-contrast: white;
|
|
687
|
+
--uniplat-chat-background: white;
|
|
688
|
+
--uniplat-chat-surface: #f7fafd;
|
|
689
|
+
--uniplat-chat-text: #25303f;
|
|
690
|
+
--uniplat-chat-muted: #8b98a6;
|
|
691
|
+
--uniplat-chat-border: #e4ebf3;
|
|
692
|
+
--uniplat-chat-error: #b42318;
|
|
693
|
+
--uniplat-chat-font-family: system-ui, sans-serif;
|
|
694
|
+
--uniplat-chat-width: 420px;
|
|
695
|
+
--uniplat-chat-height: 100dvh;
|
|
696
|
+
--uniplat-chat-radius: 0;
|
|
697
|
+
--uniplat-chat-z-index: 500;
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
主要 `::part`:`container`、`header`、`session-list-button`、`session-header`、`session-back-button`、`session-list`、`session-item`、`session-item-current`、`session-list-loading`、`session-list-empty`、`session-list-error`、`close-button`、`status`、`messages`、`time-marker`、`message`、`message-user`、`message-assistant`、`message-system`、`message-error`、`composer`、`input`、`send-button`、`human-service-button`、`empty`、`loading`、`activity` 和 `error`。
|
|
702
|
+
|
|
703
|
+
## Vue 和 Nuxt
|
|
704
|
+
|
|
705
|
+
Vue 模板可以直接使用自定义元素,并在 Vite 配置中声明:
|
|
706
|
+
|
|
707
|
+
```ts
|
|
708
|
+
// vite.config.ts
|
|
709
|
+
vue({
|
|
710
|
+
template: {
|
|
711
|
+
compilerOptions: {
|
|
712
|
+
isCustomElement: (tag) => tag === 'uniplat-ai-employee-chat',
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
})
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
Nuxt v0.1 仅在客户端注册,例如创建 `plugins/ai-employee.client.ts`:
|
|
719
|
+
|
|
720
|
+
```ts
|
|
721
|
+
export default defineNuxtPlugin(async () => {
|
|
722
|
+
await import('@uniplat/ai-employee-web-components')
|
|
723
|
+
})
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
页面中用 `<ClientOnly>` 包裹组件,不要求服务端渲染聊天内容。
|
|
727
|
+
|
|
728
|
+
## 实时协议与安全边界
|
|
729
|
+
|
|
730
|
+
客户端只允许无 query、无 hash、无用户凭据的 `ws:` / `wss:` URL。连接打开后,第一帧是 `authenticate`,收到 `realtime_ready` 后才订阅 `ai_employee` 的当前 `session` 并启动服务端指定间隔的心跳。`4401` 仅用于 JWT 无效或过期,并触发重新获取 JWT 和指数退避重连;`4400` / `4403` 被视为不可自动恢复的协议或权限错误。服务端暂不可用使用 `1013`,最多自动重连 3 次;认证错误帧中的 `SERVICE_*`、`KANBAN_AUTH_FAILED` 与 `REALTIME_UNAVAILABLE` 也按该规则处理,不会误报为 JWT 已失效。
|
|
731
|
+
|
|
732
|
+
JWT 不进入 URL、DOM、CustomEvent、localStorage 或日志。SDK 不接收可信的 `org_id`、`user_id`、角色或内部 HMAC 密钥;身份完全以 Kanban/HR-SaaS 的服务端鉴权结果为准。组件对后端错误正文和认证异常只显示固定的安全提示。
|
|
733
|
+
|
|
734
|
+
生产接入必须使用 `wss:`。`ws:` 仅用于本机开发环境。
|