@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,271 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { afterEach, describe, expect, it, vi } from 'vitest'
4
+ import { USER_MENU_TAG, UniplatUserMenu, type UserMenuConfig, type UserMenuEventMap } from '../src'
5
+
6
+ const CONFIG: UserMenuConfig = {
7
+ displayName: '张伟',
8
+ maskedMobile: '138****6688',
9
+ currentOrganizationId: 'company-beijing',
10
+ organizations: [
11
+ {
12
+ id: 'company-beijing',
13
+ name: '北京星河科技有限公司',
14
+ type: 'company',
15
+ avatarText: '星',
16
+ role: '超管',
17
+ memberCount: 128,
18
+ verificationStatus: 'verified',
19
+ },
20
+ {
21
+ id: 'company-shanghai',
22
+ name: '星河(上海)信息服务分公司',
23
+ type: 'company',
24
+ avatarText: '蓝',
25
+ role: '管理员',
26
+ memberCount: 46,
27
+ verificationStatus: 'unverified',
28
+ },
29
+ {
30
+ id: 'team-growth',
31
+ name: '星河增长项目组',
32
+ type: 'team',
33
+ avatarText: '增',
34
+ role: '团队负责人',
35
+ memberCount: 9,
36
+ },
37
+ ],
38
+ }
39
+
40
+ async function settle(menu: UniplatUserMenu): Promise<void> {
41
+ await menu.updateComplete
42
+ await Promise.resolve()
43
+ }
44
+
45
+ describe('<uniplat-user-menu>', () => {
46
+ afterEach(() => {
47
+ document.body.replaceChildren()
48
+ })
49
+
50
+ it('renders the updated 40.1 organization switch panel', async () => {
51
+ expect(customElements.get(USER_MENU_TAG)).toBe(UniplatUserMenu)
52
+ const menu = document.createElement(USER_MENU_TAG)
53
+ menu.configure(CONFIG)
54
+ document.body.append(menu)
55
+ await settle(menu)
56
+
57
+ expect(menu.shadowRoot?.querySelector('.profile-name')?.textContent).toBe('张伟')
58
+ expect(menu.shadowRoot?.querySelector('.profile-mobile')?.textContent).toBe('138****6688')
59
+ expect(menu.shadowRoot?.textContent).toContain('个人账号设置')
60
+ expect(menu.shadowRoot?.textContent).toContain('我的员工信息')
61
+ expect(menu.shadowRoot?.querySelector('.menu-action-subtitle')?.textContent).toContain(
62
+ '在 北京星河科技有限公司 的员工信息',
63
+ )
64
+ expect(menu.shadowRoot?.textContent).toContain('切换企业 / 团队(3)')
65
+ expect(menu.shadowRoot?.textContent).toContain('创建企业 / 团队')
66
+ expect(menu.shadowRoot?.textContent).toContain('退出登录')
67
+ expect(menu.shadowRoot?.querySelectorAll('.organization')).toHaveLength(3)
68
+ expect(menu.shadowRoot?.querySelector('.verification-status.verified')?.textContent).toContain(
69
+ '已认证',
70
+ )
71
+ expect(
72
+ menu.shadowRoot?.querySelector('.verification-status.unverified')?.textContent,
73
+ ).toContain('未认证')
74
+ expect(menu.shadowRoot?.querySelector('.verification-status.team')?.textContent).toContain(
75
+ '无需认证',
76
+ )
77
+ const panel = menu.shadowRoot?.querySelector<HTMLElement>('.panel')
78
+ const avatar = menu.shadowRoot?.querySelector<HTMLElement>('.avatar')
79
+ const sectionTitle = menu.shadowRoot?.querySelector<HTMLElement>('.section-title')
80
+ const list = menu.shadowRoot?.querySelector<HTMLElement>('.organization-list')
81
+ const selected = menu.shadowRoot?.querySelector<HTMLElement>('.organization.selected')
82
+ const organizationIcon = selected?.querySelector<HTMLElement>('.organization-icon')
83
+ expect(getComputedStyle(panel!).width).toBe('100%')
84
+ expect(String(UniplatUserMenu.styles)).toContain('--uniplat-user-menu-width: 330px')
85
+ expect(getComputedStyle(avatar!).width).toBe('40px')
86
+ expect(getComputedStyle(sectionTitle!).paddingTop).toBe('10px')
87
+ expect(getComputedStyle(sectionTitle!).paddingLeft).toBe('10px')
88
+ expect(getComputedStyle(sectionTitle!).fontSize).toBe('11.5px')
89
+ expect(getComputedStyle(sectionTitle!).fontWeight).toBe('700')
90
+ expect(getComputedStyle(list!).gap).toBe('6px')
91
+ expect(getComputedStyle(list!).scrollbarGutter).not.toBe('stable')
92
+ expect(getComputedStyle(selected!).borderTopWidth).toBe('1.5px')
93
+ expect(getComputedStyle(selected!).display).toBe('flex')
94
+ expect(getComputedStyle(selected!).paddingTop).toBe('9px')
95
+ expect(String(UniplatUserMenu.styles)).toContain('margin: 6px 4px 0')
96
+ expect(getComputedStyle(organizationIcon!).width).toBe('30px')
97
+ expect(organizationIcon?.querySelector('svg[data-prototype-icon="building"]')).not.toBeNull()
98
+ const selectedCheck = selected?.querySelector(
99
+ '.check svg[data-prototype-icon="circle-check-filled"]',
100
+ )
101
+ expect(selectedCheck).not.toBeNull()
102
+ expect(selectedCheck?.querySelector('path')?.namespaceURI).toBe('http://www.w3.org/2000/svg')
103
+ expect(getComputedStyle(selectedCheck!.parentElement!).visibility).toBe('visible')
104
+ expect(selectedCheck?.querySelector('path')?.getAttribute('d')).toBe(
105
+ 'M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z',
106
+ )
107
+ expect(
108
+ selected?.querySelector(
109
+ '.verification-status svg[data-prototype-icon="discount-check-filled"]',
110
+ ),
111
+ ).not.toBeNull()
112
+ const unverified = menu.shadowRoot?.querySelector<HTMLElement>(
113
+ '[data-organization-id="company-shanghai"]',
114
+ )
115
+ expect(
116
+ unverified?.querySelector('.organization-icon svg[data-prototype-icon="building"]'),
117
+ ).not.toBeNull()
118
+ expect(unverified?.querySelector('.organization-meta.attention')?.textContent).toContain(
119
+ '认证后可办理业务',
120
+ )
121
+ expect(
122
+ unverified?.querySelector('.verification-status svg[data-prototype-icon="alert-circle"]'),
123
+ ).not.toBeNull()
124
+ expect(
125
+ menu.shadowRoot?.querySelector('.verification-status.team svg[data-prototype-icon="users"]'),
126
+ ).not.toBeNull()
127
+ expect(
128
+ menu.shadowRoot?.querySelector(
129
+ '.account-settings-action svg[data-prototype-icon="user-cog"]',
130
+ ),
131
+ ).not.toBeNull()
132
+ expect(
133
+ menu.shadowRoot?.querySelector('.employee-info-action svg[data-prototype-icon="id-badge-2"]'),
134
+ ).not.toBeNull()
135
+ expect(
136
+ menu.shadowRoot?.querySelector('.create-action svg[data-prototype-icon="plus"]'),
137
+ ).not.toBeNull()
138
+ expect(
139
+ menu.shadowRoot?.querySelector('.logout svg[data-prototype-icon="logout"]'),
140
+ ).not.toBeNull()
141
+ })
142
+
143
+ it('emits only controlled actions without display data', async () => {
144
+ const menu = document.createElement(USER_MENU_TAG)
145
+ menu.configure(CONFIG)
146
+ document.body.append(menu)
147
+ await settle(menu)
148
+
149
+ const received: Array<{ type: keyof UserMenuEventMap; detail: unknown; composed: boolean }> = []
150
+ const record = (event: Event) => {
151
+ received.push({
152
+ type: event.type as keyof UserMenuEventMap,
153
+ detail: (event as CustomEvent).detail,
154
+ composed: event.composed,
155
+ })
156
+ }
157
+ document.addEventListener('organization-selected', record, { once: true })
158
+ document.addEventListener('account-settings-requested', record, { once: true })
159
+ document.addEventListener('employee-info-requested', record, { once: true })
160
+ document.addEventListener('organization-create-requested', record, { once: true })
161
+ document.addEventListener('logout-requested', record, { once: true })
162
+
163
+ menu.shadowRoot?.querySelector<HTMLButtonElement>('.account-settings-action')?.click()
164
+ menu.shadowRoot?.querySelector<HTMLButtonElement>('.employee-info-action')?.click()
165
+ menu.shadowRoot
166
+ ?.querySelector<HTMLButtonElement>('[data-organization-id="team-growth"]')
167
+ ?.click()
168
+ menu.shadowRoot?.querySelector<HTMLButtonElement>('.create-action')?.click()
169
+ menu.shadowRoot?.querySelector<HTMLButtonElement>('.logout')?.click()
170
+
171
+ expect(received).toEqual([
172
+ { type: 'account-settings-requested', detail: {}, composed: true },
173
+ {
174
+ type: 'employee-info-requested',
175
+ detail: { organizationId: 'company-beijing' },
176
+ composed: true,
177
+ },
178
+ {
179
+ type: 'organization-selected',
180
+ detail: { organizationId: 'team-growth' },
181
+ composed: true,
182
+ },
183
+ { type: 'organization-create-requested', detail: {}, composed: true },
184
+ { type: 'logout-requested', detail: {}, composed: true },
185
+ ])
186
+ expect(JSON.stringify(received)).not.toContain('138****6688')
187
+ expect(JSON.stringify(received)).not.toContain('北京星河科技有限公司')
188
+ })
189
+
190
+ it('keeps selection Host-controlled and replaces stale content when reconfigured', async () => {
191
+ const menu = document.createElement(USER_MENU_TAG)
192
+ const selected = vi.fn()
193
+ menu.addEventListener('organization-selected', selected)
194
+ menu.configure(CONFIG)
195
+ document.body.append(menu)
196
+ await settle(menu)
197
+
198
+ menu.shadowRoot
199
+ ?.querySelector<HTMLButtonElement>('[data-organization-id="team-growth"]')
200
+ ?.click()
201
+ expect(menu.shadowRoot?.querySelector('.organization.selected')?.textContent).toContain(
202
+ '北京星河科技有限公司',
203
+ )
204
+
205
+ menu.configure({
206
+ displayName: '李明',
207
+ organizations: [
208
+ { id: 'team-product', name: '产品团队', type: 'team', role: '成员', memberCount: 5 },
209
+ ],
210
+ currentOrganizationId: 'team-product',
211
+ })
212
+ await settle(menu)
213
+ expect(selected).toHaveBeenCalledTimes(1)
214
+ expect(menu.shadowRoot?.textContent).toContain('李明')
215
+ expect(menu.shadowRoot?.textContent).toContain('产品团队')
216
+ expect(menu.shadowRoot?.textContent).not.toContain('北京星河科技有限公司')
217
+ })
218
+
219
+ it('renders a safe profile avatar and keeps the text fallback when loading fails', async () => {
220
+ const menu = document.createElement(USER_MENU_TAG)
221
+ menu.configure({
222
+ ...CONFIG,
223
+ avatarText: '张',
224
+ avatarUrl: 'https://static.example.com/employee.png',
225
+ })
226
+ document.body.append(menu)
227
+ await settle(menu)
228
+
229
+ const image = menu.shadowRoot?.querySelector<HTMLImageElement>('.avatar-image')
230
+ expect(image?.src).toBe('https://static.example.com/employee.png')
231
+ expect(image?.referrerPolicy).toBe('no-referrer')
232
+ image?.dispatchEvent(new Event('error'))
233
+ expect(image?.hidden).toBe(true)
234
+ expect(menu.shadowRoot?.querySelector('.avatar-fallback')?.textContent).toBe('张')
235
+ image?.dispatchEvent(new Event('load'))
236
+ expect(image?.hidden).toBe(false)
237
+
238
+ expect(() => menu.configure({ ...CONFIG, avatarUrl: 'javascript:alert(1)' })).toThrow(TypeError)
239
+ })
240
+
241
+ it('rejects raw mobile numbers, duplicate ids and invalid organization counts', () => {
242
+ const menu = document.createElement(USER_MENU_TAG)
243
+ expect(() => menu.configure({ ...CONFIG, maskedMobile: '13800138000' })).toThrow(TypeError)
244
+ expect(() =>
245
+ menu.configure({
246
+ ...CONFIG,
247
+ organizations: [CONFIG.organizations[0]!, CONFIG.organizations[0]!],
248
+ }),
249
+ ).toThrow(TypeError)
250
+ expect(() => menu.configure({ ...CONFIG, organizationCount: 1 })).toThrow(TypeError)
251
+ })
252
+
253
+ it('renders external text as text instead of executable HTML', async () => {
254
+ const menu = document.createElement(USER_MENU_TAG)
255
+ menu.configure({
256
+ displayName: '<script>unsafe()</script>',
257
+ organizations: [
258
+ {
259
+ id: 'safe-id',
260
+ name: '<img src=x onerror=unsafe()>',
261
+ type: 'company',
262
+ },
263
+ ],
264
+ })
265
+ document.body.append(menu)
266
+ await settle(menu)
267
+ expect(menu.shadowRoot?.querySelector('script')).toBeNull()
268
+ expect(menu.shadowRoot?.querySelector('img')).toBeNull()
269
+ expect(menu.shadowRoot?.textContent).toContain('<img src=x onerror=unsafe()>')
270
+ })
271
+ })
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "experimentalDecorators": true
6
+ },
7
+ "include": ["src", "tests", "tsup.config.ts", "tsup.runtime.config.ts"]
8
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm'],
6
+ dts: true,
7
+ sourcemap: true,
8
+ clean: true,
9
+ target: 'es2022',
10
+ treeshake: true,
11
+ external: ['lit', '@uniplat/ai-employee-client'],
12
+ })
@@ -0,0 +1,37 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { defineConfig } from 'tsup'
4
+
5
+ interface PackageManifest {
6
+ version: string
7
+ }
8
+
9
+ const packageEntry = fileURLToPath(new URL('./src/index.ts', import.meta.url)).replaceAll('\\', '/')
10
+ const packageManifest = JSON.parse(
11
+ await readFile(new URL('./package.json', import.meta.url), 'utf8'),
12
+ ) as PackageManifest
13
+ const version = packageManifest.version.trim()
14
+
15
+ if (!/^[0-9A-Za-z][0-9A-Za-z._+-]{0,63}$/.test(version)) {
16
+ throw new TypeError('Web Components package version cannot be used as a runtime release version')
17
+ }
18
+
19
+ const releaseDirectory = fileURLToPath(
20
+ new URL(`../../dist/runtime/releases/${version}/`, import.meta.url),
21
+ )
22
+
23
+ export default defineConfig({
24
+ entry: [packageEntry],
25
+ format: ['esm'],
26
+ platform: 'browser',
27
+ target: 'es2022',
28
+ outDir: releaseDirectory,
29
+ outExtension: () => ({ js: '.js' }),
30
+ bundle: true,
31
+ splitting: false,
32
+ dts: false,
33
+ sourcemap: true,
34
+ clean: true,
35
+ treeshake: true,
36
+ noExternal: ['lit', '@uniplat/ai-employee-client'],
37
+ })
@@ -0,0 +1,6 @@
1
+ packages:
2
+ - packages/*
3
+ - examples/*
4
+
5
+ allowBuilds:
6
+ esbuild: true
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "moduleResolution": "Bundler",
8
+ "strict": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "noImplicitOverride": true,
11
+ "useUnknownInCatchVariables": true,
12
+ "verbatimModuleSyntax": true,
13
+ "isolatedModules": true,
14
+ "skipLibCheck": true,
15
+ "baseUrl": ".",
16
+ "paths": {
17
+ "@uniplat/ai-employee-client": ["packages/core/src/index.ts"],
18
+ "@uniplat/ai-employee-web-components": ["packages/web-components/src/index.ts"]
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,20 @@
1
+ import { fileURLToPath, URL } from 'node:url'
2
+ import { defineConfig } from 'vitest/config'
3
+
4
+ export default defineConfig({
5
+ resolve: {
6
+ alias: {
7
+ '@uniplat/ai-employee-client': fileURLToPath(
8
+ new URL('./packages/core/src/index.ts', import.meta.url),
9
+ ),
10
+ '@uniplat/ai-employee-web-components': fileURLToPath(
11
+ new URL('./packages/web-components/src/index.ts', import.meta.url),
12
+ ),
13
+ },
14
+ },
15
+ test: {
16
+ include: ['packages/**/*.test.ts'],
17
+ restoreMocks: true,
18
+ clearMocks: true,
19
+ },
20
+ })
@@ -0,0 +1,254 @@
1
+ # Uni-Base 项目交接说明
2
+
3
+ ## 1. 当前仓库状态
4
+
5
+ - 仓库:`/Users/gaostudio/uni-base`
6
+ - 分支:`main`
7
+ - 交接时分支:`main`,客服、通知中心、发布说明等提交已通过完整验证并随本次交接推送。
8
+ - 根目录历次 `pnpm pack` 生成的 `.tgz` 是本地发布产物,已由 `.gitignore` 忽略;不要提交或覆盖调用方正在使用的包。
9
+ - 当前版本:
10
+ - Core:`@uniplat/ai-employee-client@0.12.0`
11
+ - Web Components:`@uniplat/ai-employee-web-components@0.19.0`
12
+ - 本地 runtime manifest:`0.19.0`
13
+ - `https://10.10.10.131:8787` 在本次交接时已启动,`current.json` 与 `releases/0.19.0/index.js` 可匿名读取。
14
+ - npm registry 是 `https://registry.npmmirror.com`,当前机器未登录,npm 发布受认证条件阻塞。
15
+
16
+ 下一位 Agent 开始前必须完整阅读仓库根目录的 `AGENTS.md`,它替代所有旧版规则。
17
+
18
+ ## 2. 当前公开 Web Components
19
+
20
+ ```html
21
+ <uniplat-ai-employee-chat></uniplat-ai-employee-chat>
22
+ <uniplat-customer-support-chat></uniplat-customer-support-chat>
23
+ <uniplat-user-login></uniplat-user-login>
24
+ <uniplat-organization-selector></uniplat-organization-selector>
25
+ <uniplat-account-context></uniplat-account-context>
26
+ <uniplat-user-menu></uniplat-user-menu>
27
+ <uniplat-notification-center></uniplat-notification-center>
28
+ ```
29
+
30
+ 以下标签不再公开或注册:
31
+
32
+ ```html
33
+ <uniplat-application-switcher></uniplat-application-switcher>
34
+ <uniplat-organization-onboarding></uniplat-organization-onboarding>
35
+ ```
36
+
37
+ 工作台应用面板和组织入驻流程均内置于 `<uniplat-account-context>`。
38
+
39
+ ## 3. 数字员工聊天
40
+
41
+ `<uniplat-ai-employee-chat>` 渲染完整右侧抽屉,Host 只负责 `configure()` 和 `open()`,不能再在外部拼装标题、消息区或输入区。
42
+
43
+ 关键规则:
44
+
45
+ - 支持登录用户和匿名用户。
46
+ - WebSocket 使用 `/api/user-realtime/ws` 的 `ai_employee` channel。
47
+ - 同一连接只订阅当前 AI 会话,切换会话时取消旧订阅。
48
+ - 打开抽屉或点击新对话只建立本地草稿。
49
+ - 首条消息通过 `createSessionWithFirstMessage()` 原子创建会话。
50
+ - 不允许先创建空 Session 再发送首条消息。
51
+ - 当前会话始终视为已读,不显示角标,也不计入总未读。
52
+ - “转人工”直接在同一抽屉切换到 `<uniplat-customer-support-chat>`。
53
+
54
+ ## 4. 人工客服组件
55
+
56
+ 主要实现:
57
+
58
+ - `packages/core/src/customer-support-client.ts`
59
+ - `packages/web-components/src/customer-support-chat.ts`
60
+ - `packages/web-components/src/customer-support-transport.ts`
61
+
62
+ 配置方式:
63
+
64
+ ```ts
65
+ customerSupportChat.configure({
66
+ realtimeUrl,
67
+ transport,
68
+ sessionId,
69
+ })
70
+
71
+ customerSupportChat.open()
72
+ ```
73
+
74
+ ### 4.1 实时连接规则
75
+
76
+ 人工客服使用同一个 `/api/user-realtime/ws`,但申请:
77
+
78
+ ```json
79
+ {
80
+ "requested_channels": ["customer_support"]
81
+ }
82
+ ```
83
+
84
+ 服务端按当前认证身份推送该用户所有客服会话的事件,而不是只推送当前会话。
85
+
86
+ 最新完成的处理逻辑:
87
+
88
+ - 任意客服会话事件都会重新请求会话列表。
89
+ - 其他会话产生消息:
90
+ - 更新会话标题、最后消息、状态和未读数。
91
+ - 不刷新当前聊天历史。
92
+ - 更新顶部聚合红点及会话列表红点。
93
+ - 当前会话产生消息:
94
+ - 刷新会话列表。
95
+ - 刷新当前历史消息。
96
+ - 自动同步已读。
97
+ - `support_read_state_changed`:
98
+ - 只刷新会话列表和未读投影。
99
+ - 不刷新历史。
100
+ - 不再次触发已读写入,避免反馈循环。
101
+ - 当前正在查看的会话:
102
+ - 不显示未读红点。
103
+ - 不计入顶部聚合未读数量。
104
+
105
+ 相关提交:
106
+
107
+ ```text
108
+ 2b5d5b9 fix(customer-support): 实时刷新全部客服会话与未读提示
109
+ b65dcb9 fix(customer-support): 阻断客服已读事件反馈环并实现游标幂等
110
+ ```
111
+
112
+ ### 4.2 客服 UI 状态
113
+
114
+ 标题:`企业客服人工顾问`
115
+
116
+ 状态由“会话状态 + 实时连接状态”组合:
117
+
118
+ - 尚无会话、连接正常:`客服通道已连接`
119
+ - 尚无会话、连接中:`正在连接客服通道`
120
+ - `waiting`:`等待客服接入`
121
+ - `active` 且实时正常:`人工客服在线`
122
+ - `active` 但实时断开:`正在恢复实时连接`
123
+ - `closed`:`会话已结束`
124
+
125
+ 提交 `cca4db1` 修复了连接恢复后仍残留“鉴权服务暂不可用”横幅的问题:连接状态恢复为 `connected` 时清空可恢复错误提示。
126
+
127
+ ## 5. 通知中心组件
128
+
129
+ `<uniplat-notification-center>` 是通知收件箱 Web Component,提供分类目录、通知列表与详情、未读状态、全部已读、关联资源和附件下载。
130
+
131
+ 关键边界:
132
+
133
+ - 组件和 Core 不建立 WebSocket。
134
+ - Host 复用自己的 `/api/user-realtime/ws`,订阅 `notification` channel,并通过 `applyRealtimeEvent(rawEvent)` 把事件交给组件。
135
+ - 分类目录随 `listInbox({ includeTypeFacets: true })` 一并加载,不额外请求接口。
136
+ - 分类显示名称来自后端 `notification_type_name`,SDK 不维护中文名映射。
137
+ - 真实 HTTP transport 使用 `createUniplatBaseNotificationCenterTransport()`;JWT 只进入 transport 内部的 Authorization header。
138
+ - 当前版本 Core `0.12.0`、Web Components `0.19.0` 已包含通知中心实现、测试与独立浏览器运行时。
139
+
140
+ ## 6. 登录、账户和工作台
141
+
142
+ ### 6.1 User Login
143
+
144
+ 必须严格复刻 40.1 原型:
145
+
146
+ - 密码登录。
147
+ - 短信验证码登录。
148
+ - 扫码登录。
149
+ - 不显示图形验证码。
150
+ - 发送短信按钮始终可见,仅在手机号不完整或倒计时时禁用。
151
+ - 登录结果先交给 Host `setAccessToken()`,再派发无凭据事件。
152
+ - 登录设备信息包括稳定浏览器设备指纹,不得每次随机生成 `device_id`。
153
+
154
+ ### 6.2 Account Context
155
+
156
+ `<uniplat-account-context>` 负责:
157
+
158
+ - 用户姓名、头像、脱敏手机号。
159
+ - 当前组织和可选组织。
160
+ - 企业认证状态。
161
+ - 员工信息弹窗。
162
+ - 创建企业/团队流程。
163
+ - 内嵌工作台应用面板。
164
+ - 应用 ticket 跳转。
165
+
166
+ 必须传入 `applicationName`。账户资料从 `list_available_organizations` 顶层读取:
167
+
168
+ - `display_name`
169
+ - `avatar_url`
170
+ - `mobile_masked`
171
+
172
+ 不得再调用 `profile_summary_v2`。
173
+
174
+ 工作台只请求应用列表,不请求组织列表;组织上下文由 Account Context 管理。
175
+
176
+ 应用跳转流程:
177
+
178
+ 1. 先构造完整目标 URL。
179
+ 2. 使用当前 JWT 以 `POST {}` 调用 `issue_ticket`。
180
+ 3. 从签发响应取得 Ticket、15 秒有效期和权威 `source_system`。
181
+ 4. 新窗口打开目标页面。
182
+ 5. Ticket 放入 `#uniplat_identity_ticket=<ticket>&source_system=<source_system>`。
183
+ 6. 目标页先清除 Ticket Fragment,再以项目初始化的 `applicationName` 调用 `login_with_ticket`。
184
+ 7. JWT 交给目标 Host 的 `setAccessToken()`。
185
+
186
+ ## 7. 关联项目与资料
187
+
188
+ 跨项目工作前先读取:
189
+
190
+ ```text
191
+ /Users/gaostudio/.vibe-requirements/projects.md
192
+ ```
193
+
194
+ 主要关联项目:
195
+
196
+ - Kanban 后端:`/Users/gaostudio/ae_design/实现/uniplat-kanban/backend`
197
+ - HR-SaaS:`/Users/gaostudio/ae_design/实现/hr_saas`
198
+ - UniplatBase:`/Users/gaostudio/ae_design/实现/uniplat_base`
199
+ - 官网:`/Users/gaostudio/qqxbwebsite`
200
+ - Enterprise Admin:`/Users/gaostudio/ae_design/实现/ae-enterprise-admin`
201
+
202
+ 40.1 总原型:
203
+
204
+ ```text
205
+ /Users/gaostudio/qqxbwebsite/原型/40.1、注册登录与企业入驻流程-standalone.html
206
+ ```
207
+
208
+ 跨项目只允许查阅。未经用户明确授权,不得直接修改其他项目;如需后端或 Host 配合,应在 `.vibe-requirements` 编写独立联合需求。
209
+
210
+ ## 8. 验证与发布要求
211
+
212
+ 任何代码修改后必须执行:
213
+
214
+ ```bash
215
+ pnpm format:check
216
+ pnpm lint
217
+ pnpm typecheck
218
+ pnpm test
219
+ pnpm build
220
+ pnpm --filter @uniplat/ai-employee-client pack
221
+ pnpm --filter @uniplat/ai-employee-web-components pack
222
+ ```
223
+
224
+ 确认修法后直接完成:
225
+
226
+ ```text
227
+ 实现 → 测试 → 构建 → 打包 → 版本递增 → 提交 → 发布
228
+ ```
229
+
230
+ 不要在提出修法后停下来等待重复确认。
231
+
232
+ 发布时:
233
+
234
+ - 先发布并验证版本化 runtime 目录。
235
+ - 再原子切换 `current.json`。
236
+ - 发布后重新读取远端 manifest 和模块校验。
237
+ - npm 未认证时明确报告外部阻塞,不得声称 npm 发布成功。
238
+ - 不要提交根目录历史 tarball。
239
+
240
+ ## 9. 下一步建议
241
+
242
+ 下一位 Agent 应优先确认当前远端与发布状态:
243
+
244
+ 1. 核对 `main` 已推送到 `origin/main`。
245
+ 2. 运行完整验证,确认 Core `0.12.0`、Web Components `0.19.0`。
246
+ 3. 确认 HTTPS runtime 服务仍在运行;未运行时执行 `node /Users/gaostudio/.uni-base-runtime/serve.mjs`。
247
+ 4. 验证:
248
+
249
+ ```text
250
+ https://10.10.10.131:8787/current.json
251
+ https://10.10.10.131:8787/releases/0.19.0/index.js
252
+ ```
253
+
254
+ 5. npm 发布仍需用户先为配置的 registry 完成登录。