@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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export type NotificationReadStatus = 'unread' | 'read'
|
|
2
|
+
export type NotificationContentType = 'text' | 'markdown'
|
|
3
|
+
|
|
4
|
+
export interface NotificationSource {
|
|
5
|
+
type: string
|
|
6
|
+
code: string
|
|
7
|
+
id: string
|
|
8
|
+
name: string
|
|
9
|
+
avatarUrl: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface NotificationResource {
|
|
13
|
+
type: string
|
|
14
|
+
id: string
|
|
15
|
+
title: string
|
|
16
|
+
aeCode: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface NotificationAttachment {
|
|
20
|
+
fileId: string
|
|
21
|
+
versionId: string
|
|
22
|
+
name: string
|
|
23
|
+
sizeBytes: number
|
|
24
|
+
mimeType: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface NotificationDto {
|
|
28
|
+
id: string
|
|
29
|
+
type: string
|
|
30
|
+
// 后端 notification_type_name 投影;缺省为空串,展示时回退到 type。
|
|
31
|
+
typeName: string
|
|
32
|
+
title: string
|
|
33
|
+
summary: string
|
|
34
|
+
content: string
|
|
35
|
+
contentType: NotificationContentType
|
|
36
|
+
priority: string
|
|
37
|
+
source: NotificationSource | null
|
|
38
|
+
resources: readonly NotificationResource[]
|
|
39
|
+
attachments: readonly NotificationAttachment[]
|
|
40
|
+
readStatus: NotificationReadStatus
|
|
41
|
+
readAt: string | null
|
|
42
|
+
archivedAt: string | null
|
|
43
|
+
createdAt: string
|
|
44
|
+
expiresAt: string | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 分类目录条目:稳定 code 加后端字典给出的展示名(可能为空)。 */
|
|
48
|
+
export interface NotificationTypeSummary {
|
|
49
|
+
type: string
|
|
50
|
+
name: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface NotificationInboxQuery {
|
|
54
|
+
pageSize: number
|
|
55
|
+
beforeId?: string | null
|
|
56
|
+
// 请求分类目录;后端按收件人可见范围统计,忽略类型筛选与分页游标。
|
|
57
|
+
includeTypeFacets?: boolean
|
|
58
|
+
notificationTypes?: readonly string[]
|
|
59
|
+
readStatus?: 'all' | 'unread' | 'read'
|
|
60
|
+
includeArchived?: boolean
|
|
61
|
+
// Empty means the current principal's notifications across all applications.
|
|
62
|
+
applicationName: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface NotificationInboxResult {
|
|
66
|
+
items: readonly NotificationDto[]
|
|
67
|
+
hasMore: boolean
|
|
68
|
+
nextId: string | null
|
|
69
|
+
// 仅在 includeTypeFacets 请求且后端支持时出现;缺省时客户端回退为累积已加载分类。
|
|
70
|
+
types?: readonly NotificationTypeSummary[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface NotificationMarkReadResult {
|
|
74
|
+
unreadCount: number
|
|
75
|
+
readAt: string | null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface NotificationMarkAllReadResult {
|
|
79
|
+
markedCount: number
|
|
80
|
+
unreadCount: number
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type NotificationRealtimeChange =
|
|
84
|
+
| { kind: 'created'; notification: NotificationDto; unreadCount?: number }
|
|
85
|
+
| {
|
|
86
|
+
kind: 'read'
|
|
87
|
+
notificationIds: readonly string[]
|
|
88
|
+
unreadCount?: number
|
|
89
|
+
readAt?: string | null
|
|
90
|
+
}
|
|
91
|
+
| { kind: 'archived'; notificationIds: readonly string[]; unreadCount?: number }
|
|
92
|
+
|
|
93
|
+
export interface NotificationCenterTransport {
|
|
94
|
+
listInbox(query: NotificationInboxQuery): Promise<NotificationInboxResult>
|
|
95
|
+
getUnreadCount(params: { applicationName: string }): Promise<{ unreadCount: number }>
|
|
96
|
+
markRead(params: {
|
|
97
|
+
notificationId: string
|
|
98
|
+
applicationName: string
|
|
99
|
+
}): Promise<NotificationMarkReadResult>
|
|
100
|
+
markAllRead(params: {
|
|
101
|
+
beforeId?: string | null
|
|
102
|
+
notificationTypes?: readonly string[]
|
|
103
|
+
applicationName: string
|
|
104
|
+
}): Promise<NotificationMarkAllReadResult>
|
|
105
|
+
downloadAttachment(params: { fileId: string; name?: string }): Promise<Blob>
|
|
106
|
+
archive?(params: {
|
|
107
|
+
notificationId: string
|
|
108
|
+
applicationName: string
|
|
109
|
+
}): Promise<{ unreadCount: number }>
|
|
110
|
+
// Optional: map a raw realtime notification event handed in by the host to a
|
|
111
|
+
// stable DTO change so the inbox can update in place. When absent — or when the
|
|
112
|
+
// event cannot be mapped — the client falls back to a full HTTP refresh.
|
|
113
|
+
parseRealtimeEvent?(event: unknown): NotificationRealtimeChange | null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface NotificationCenterClientOptions {
|
|
117
|
+
transport: NotificationCenterTransport
|
|
118
|
+
// Empty means the current principal's notifications across all applications.
|
|
119
|
+
applicationName: string
|
|
120
|
+
pageSize?: number
|
|
121
|
+
// Optional inbox projection used by compact consumers such as the unread
|
|
122
|
+
// notification popover. The full notification center defaults to `all`.
|
|
123
|
+
readStatus?: 'all' | 'unread' | 'read'
|
|
124
|
+
// Optional category codes pinned to the front of the tab order when they exist
|
|
125
|
+
// in the data. Defaults to empty: order then follows the backend catalog, and
|
|
126
|
+
// the SDK privileges no code of its own.
|
|
127
|
+
preferredTypeOrder?: readonly string[]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type NotificationCenterErrorCode =
|
|
131
|
+
| 'INVALID_CONFIGURATION'
|
|
132
|
+
| 'INBOX_LOAD_FAILED'
|
|
133
|
+
| 'MARK_READ_FAILED'
|
|
134
|
+
| 'DOWNLOAD_FAILED'
|
|
135
|
+
| 'CLIENT_DESTROYED'
|
|
136
|
+
|
|
137
|
+
export interface NotificationCenterErrorDetail {
|
|
138
|
+
code: NotificationCenterErrorCode
|
|
139
|
+
message: string
|
|
140
|
+
recoverable: boolean
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface NotificationCenterClientEventMap {
|
|
144
|
+
ready: Record<string, never>
|
|
145
|
+
inbox: NotificationInboxResult
|
|
146
|
+
'types-change': { types: readonly NotificationTypeSummary[] }
|
|
147
|
+
'unread-count-change': { unreadCount: number }
|
|
148
|
+
'notification-read': { notificationId: string }
|
|
149
|
+
error: NotificationCenterErrorDetail
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export type NotificationCenterClientListener<K extends keyof NotificationCenterClientEventMap> = (
|
|
153
|
+
detail: NotificationCenterClientEventMap[K],
|
|
154
|
+
) => void
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import type { AiEmployeeMessage, AiEmployeeMessageRole } from './types'
|
|
2
|
+
|
|
3
|
+
type JsonRecord = Record<string, unknown>
|
|
4
|
+
|
|
5
|
+
export interface AuthenticateFrame {
|
|
6
|
+
type: 'authenticate'
|
|
7
|
+
protocol_version: 1
|
|
8
|
+
access_token: string
|
|
9
|
+
requested_channels: ['ai_employee']
|
|
10
|
+
cursors: Record<string, never>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SessionSubscriptionFrame {
|
|
14
|
+
action: 'subscribe' | 'unsubscribe'
|
|
15
|
+
channel: 'ai_employee'
|
|
16
|
+
resource_type: 'session'
|
|
17
|
+
resource_id: string
|
|
18
|
+
replay_history?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PingFrame {
|
|
22
|
+
action: 'ping'
|
|
23
|
+
timestamp: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ParsedSessionMessage {
|
|
27
|
+
messages: AiEmployeeMessage[]
|
|
28
|
+
fallbackResult?: AiEmployeeMessage
|
|
29
|
+
processing?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ParsedAiEmployeeEvent =
|
|
33
|
+
| { kind: 'session-message'; sessionId: string; result: ParsedSessionMessage }
|
|
34
|
+
| { kind: 'session-title'; sessionId: string; title: string }
|
|
35
|
+
| { kind: 'ignored' }
|
|
36
|
+
|
|
37
|
+
export function createAuthenticateFrame(accessToken: string): AuthenticateFrame {
|
|
38
|
+
return {
|
|
39
|
+
type: 'authenticate',
|
|
40
|
+
protocol_version: 1,
|
|
41
|
+
access_token: accessToken,
|
|
42
|
+
requested_channels: ['ai_employee'],
|
|
43
|
+
cursors: {},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createSubscriptionFrame(
|
|
48
|
+
sessionId: string,
|
|
49
|
+
action: 'subscribe' | 'unsubscribe' = 'subscribe',
|
|
50
|
+
replayHistory = false,
|
|
51
|
+
): SessionSubscriptionFrame {
|
|
52
|
+
const frame: SessionSubscriptionFrame = {
|
|
53
|
+
action,
|
|
54
|
+
channel: 'ai_employee',
|
|
55
|
+
resource_type: 'session',
|
|
56
|
+
resource_id: sessionId,
|
|
57
|
+
}
|
|
58
|
+
if (action === 'subscribe') frame.replay_history = replayHistory
|
|
59
|
+
return frame
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createPingFrame(now = new Date()): PingFrame {
|
|
63
|
+
return { action: 'ping', timestamp: now.toISOString() }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function parseJsonRecord(raw: string): JsonRecord | null {
|
|
67
|
+
try {
|
|
68
|
+
return asRecord(JSON.parse(raw))
|
|
69
|
+
} catch {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function parseAiEmployeeEvent(raw: unknown): ParsedAiEmployeeEvent {
|
|
75
|
+
const event = asRecord(raw)
|
|
76
|
+
const type = stringValue(event.type)
|
|
77
|
+
const sessionId = stringValue(event.session_id).trim()
|
|
78
|
+
|
|
79
|
+
if (type === 'session_title_updated') {
|
|
80
|
+
const title = stringValue(asRecord(event.payload).title).trim()
|
|
81
|
+
return sessionId && title ? { kind: 'session-title', sessionId, title } : { kind: 'ignored' }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (type !== 'session_message' || !sessionId) return { kind: 'ignored' }
|
|
85
|
+
return {
|
|
86
|
+
kind: 'session-message',
|
|
87
|
+
sessionId,
|
|
88
|
+
result: parseSessionPayload(asRecord(event.payload), sessionId),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseSessionPayload(payload: JsonRecord, sessionId: string): ParsedSessionMessage {
|
|
93
|
+
const messages: AiEmployeeMessage[] = []
|
|
94
|
+
const patches = Array.isArray(payload.JsonPatch) ? payload.JsonPatch : []
|
|
95
|
+
|
|
96
|
+
for (const rawPatch of patches) {
|
|
97
|
+
const patch = asRecord(rawPatch)
|
|
98
|
+
const value = asRecord(patch.value)
|
|
99
|
+
const parsed = messageFromEntry(value, sessionId, patch)
|
|
100
|
+
if (parsed) messages.push(parsed)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result = stringValue(payload.result).trim()
|
|
104
|
+
const fallbackResult = result
|
|
105
|
+
? createMessage(
|
|
106
|
+
sessionId,
|
|
107
|
+
'assistant',
|
|
108
|
+
result,
|
|
109
|
+
`result:${sessionId}:${hashText(result)}`,
|
|
110
|
+
payload,
|
|
111
|
+
)
|
|
112
|
+
: undefined
|
|
113
|
+
|
|
114
|
+
if (payload.error) {
|
|
115
|
+
messages.push(
|
|
116
|
+
createMessage(
|
|
117
|
+
sessionId,
|
|
118
|
+
'error',
|
|
119
|
+
'AI 回复出现异常,请稍后重试。',
|
|
120
|
+
`error:${sessionId}:${hashText(stableString(payload))}`,
|
|
121
|
+
payload,
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let processing: boolean | undefined
|
|
127
|
+
if (payload.Ready === true) processing = true
|
|
128
|
+
if (payload.finished === true || payload.stopped === true) processing = false
|
|
129
|
+
return { messages, fallbackResult, processing }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function messageFromEntry(
|
|
133
|
+
entry: JsonRecord,
|
|
134
|
+
sessionId: string,
|
|
135
|
+
patch: JsonRecord,
|
|
136
|
+
): AiEmployeeMessage | null {
|
|
137
|
+
const entryTypeValue = entry.entry_type
|
|
138
|
+
const entryType =
|
|
139
|
+
typeof entryTypeValue === 'string' ? entryTypeValue : stringValue(asRecord(entryTypeValue).type)
|
|
140
|
+
|
|
141
|
+
let role: AiEmployeeMessageRole
|
|
142
|
+
switch (entryType) {
|
|
143
|
+
case 'assistant_message':
|
|
144
|
+
case 'result':
|
|
145
|
+
role = 'assistant'
|
|
146
|
+
break
|
|
147
|
+
case 'user_message':
|
|
148
|
+
role = 'user'
|
|
149
|
+
break
|
|
150
|
+
case 'system_message':
|
|
151
|
+
case 'system_push':
|
|
152
|
+
role = 'system'
|
|
153
|
+
break
|
|
154
|
+
case 'error':
|
|
155
|
+
case 'error_message':
|
|
156
|
+
role = 'error'
|
|
157
|
+
break
|
|
158
|
+
default:
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const content = role === 'error' ? 'AI 回复出现异常,请稍后重试。' : extractText(entry.content)
|
|
163
|
+
if (!content) return null
|
|
164
|
+
|
|
165
|
+
const messageId = stringValue(entry.message_id)
|
|
166
|
+
const webSocketMessageId = stringValue(entry.ws_message_id)
|
|
167
|
+
const explicitId =
|
|
168
|
+
role === 'user'
|
|
169
|
+
? messageId || webSocketMessageId || stringValue(entry.id)
|
|
170
|
+
: webSocketMessageId || messageId || stringValue(entry.id)
|
|
171
|
+
const fingerprint = stableString({ entry, path: patch.path, op: patch.op })
|
|
172
|
+
const id = explicitId || `realtime:${sessionId}:${hashText(fingerprint)}`
|
|
173
|
+
return createMessage(sessionId, role, content, id, entry)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function createMessage(
|
|
177
|
+
sessionId: string,
|
|
178
|
+
role: AiEmployeeMessageRole,
|
|
179
|
+
content: string,
|
|
180
|
+
id: string,
|
|
181
|
+
source: JsonRecord,
|
|
182
|
+
): AiEmployeeMessage {
|
|
183
|
+
const createdAt =
|
|
184
|
+
stringValue(source.timestamp) ||
|
|
185
|
+
stringValue(source.create_time) ||
|
|
186
|
+
stringValue(source.created_at) ||
|
|
187
|
+
new Date().toISOString()
|
|
188
|
+
return { id, sessionId, role, content, createdAt }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function extractText(raw: unknown): string {
|
|
192
|
+
if (typeof raw !== 'string') return ''
|
|
193
|
+
const value = raw.trim()
|
|
194
|
+
if (!value) return ''
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const parsed: unknown = JSON.parse(value)
|
|
198
|
+
if (typeof parsed === 'string') return parsed.trim()
|
|
199
|
+
const record = asRecord(parsed)
|
|
200
|
+
for (const key of ['text', 'result', 'message', 'content']) {
|
|
201
|
+
const candidate = stringValue(record[key]).trim()
|
|
202
|
+
if (candidate) return candidate
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
// Plain text and the simple user-message XML format are handled below.
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const messageMatch = value.match(/<message>([\s\S]*?)<\/message>/i)
|
|
209
|
+
return (messageMatch?.[1] ?? value).trim()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function asRecord(value: unknown): JsonRecord {
|
|
213
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
214
|
+
? (value as JsonRecord)
|
|
215
|
+
: {}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function stringValue(value: unknown): string {
|
|
219
|
+
return typeof value === 'string' ? value : ''
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function stableString(value: unknown): string {
|
|
223
|
+
try {
|
|
224
|
+
return JSON.stringify(value) ?? ''
|
|
225
|
+
} catch {
|
|
226
|
+
return ''
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function hashText(value: string): string {
|
|
231
|
+
let hash = 2_166_136_261
|
|
232
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
233
|
+
hash ^= value.charCodeAt(index)
|
|
234
|
+
hash = Math.imul(hash, 16_777_619)
|
|
235
|
+
}
|
|
236
|
+
return (hash >>> 0).toString(36)
|
|
237
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
export type AiEmployeeMessageRole = 'user' | 'assistant' | 'system' | 'error'
|
|
2
|
+
|
|
3
|
+
export interface AiEmployeeMessage {
|
|
4
|
+
id: string
|
|
5
|
+
sessionId: string
|
|
6
|
+
role: AiEmployeeMessageRole
|
|
7
|
+
content: string
|
|
8
|
+
createdAt: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AiEmployeeSessionSummary {
|
|
12
|
+
sessionId: string
|
|
13
|
+
title: string
|
|
14
|
+
latestMessage: string
|
|
15
|
+
latestMessageId?: string
|
|
16
|
+
updatedAt: string
|
|
17
|
+
unreadCount: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AiEmployeeAttachmentReference {
|
|
21
|
+
transactionId: string
|
|
22
|
+
fspath: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AiEmployeeFirstMessageOptions {
|
|
26
|
+
attachments?: readonly AiEmployeeAttachmentReference[]
|
|
27
|
+
trustedContext?: Readonly<Record<string, unknown>> | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AiEmployeeFirstMessageRequest {
|
|
31
|
+
requestId: string
|
|
32
|
+
aeCode: string
|
|
33
|
+
agentCode: string
|
|
34
|
+
content: string
|
|
35
|
+
attachments: readonly AiEmployeeAttachmentReference[]
|
|
36
|
+
trustedContext: Readonly<Record<string, unknown>> | null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AiEmployeeFirstMessageResult {
|
|
40
|
+
sessionId: string
|
|
41
|
+
messageId: string
|
|
42
|
+
executionId: string
|
|
43
|
+
status: 'running' | 'queued'
|
|
44
|
+
startedAt: string
|
|
45
|
+
aeCode: string
|
|
46
|
+
agentCode: string
|
|
47
|
+
conversationType: string
|
|
48
|
+
title?: string
|
|
49
|
+
createdAt?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AiEmployeeFirstMessageRequestState {
|
|
53
|
+
fingerprint: string
|
|
54
|
+
requestId: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AiEmployeeFirstMessageRequestStore {
|
|
58
|
+
load(
|
|
59
|
+
scope: string,
|
|
60
|
+
): AiEmployeeFirstMessageRequestState | null | Promise<AiEmployeeFirstMessageRequestState | null>
|
|
61
|
+
save(scope: string, state: AiEmployeeFirstMessageRequestState): void | Promise<void>
|
|
62
|
+
clear(scope: string): void | Promise<void>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AiEmployeeTransport {
|
|
66
|
+
createSessionWithFirstMessage(
|
|
67
|
+
request: AiEmployeeFirstMessageRequest,
|
|
68
|
+
): Promise<AiEmployeeFirstMessageResult>
|
|
69
|
+
listSessions?(): Promise<AiEmployeeSessionSummary[]>
|
|
70
|
+
markSessionRead?(sessionId: string, latestMessageId: string): Promise<void>
|
|
71
|
+
listMessages(sessionId: string): Promise<AiEmployeeMessage[]>
|
|
72
|
+
sendMessage(sessionId: string, content: string): Promise<void>
|
|
73
|
+
getAccessToken(): Promise<string>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type AiEmployeeTransportErrorCode =
|
|
77
|
+
| 'UNAUTHORIZED'
|
|
78
|
+
| 'ACCESS_DENIED'
|
|
79
|
+
| 'AI_EMPLOYEE_NOT_AVAILABLE'
|
|
80
|
+
| 'APPLICATION_NOT_SUPPORTED'
|
|
81
|
+
| 'ORGANIZATION_CONTEXT_REQUIRED'
|
|
82
|
+
| 'INVALID_ARGUMENT'
|
|
83
|
+
| 'IDEMPOTENCY_CONFLICT'
|
|
84
|
+
| 'REALTIME_UNAVAILABLE'
|
|
85
|
+
| 'TOO_MANY_REQUESTS'
|
|
86
|
+
|
|
87
|
+
export type AiEmployeeConnectionState =
|
|
88
|
+
| 'idle'
|
|
89
|
+
| 'connecting'
|
|
90
|
+
| 'authenticating'
|
|
91
|
+
| 'subscribing'
|
|
92
|
+
| 'connected'
|
|
93
|
+
| 'reconnecting'
|
|
94
|
+
| 'disconnected'
|
|
95
|
+
| 'destroyed'
|
|
96
|
+
|
|
97
|
+
export type AiEmployeeErrorCode =
|
|
98
|
+
| 'INVALID_CONFIGURATION'
|
|
99
|
+
| 'SESSION_UNAVAILABLE'
|
|
100
|
+
| 'HISTORY_LOAD_FAILED'
|
|
101
|
+
| 'TOKEN_UNAVAILABLE'
|
|
102
|
+
| 'WEBSOCKET_UNAVAILABLE'
|
|
103
|
+
| 'WEBSOCKET_ERROR'
|
|
104
|
+
| 'AUTHENTICATION_FAILED'
|
|
105
|
+
| 'ORGANIZATION_CONTEXT_REQUIRED'
|
|
106
|
+
| 'REALTIME_UNAVAILABLE'
|
|
107
|
+
| 'CHANNEL_UNAVAILABLE'
|
|
108
|
+
| 'SESSION_NOT_FOUND'
|
|
109
|
+
| 'PROTOCOL_ERROR'
|
|
110
|
+
| 'SEND_FAILED'
|
|
111
|
+
| 'FIRST_MESSAGE_CONFLICT'
|
|
112
|
+
| 'RATE_LIMITED'
|
|
113
|
+
| 'SEND_IN_PROGRESS'
|
|
114
|
+
| 'EMPTY_MESSAGE'
|
|
115
|
+
| 'CLIENT_DESTROYED'
|
|
116
|
+
|
|
117
|
+
export interface AiEmployeeClientErrorDetail {
|
|
118
|
+
code: AiEmployeeErrorCode
|
|
119
|
+
message: string
|
|
120
|
+
recoverable: boolean
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface AiEmployeeClientEventMap {
|
|
124
|
+
ready: { sessionId: string }
|
|
125
|
+
history: { sessionId: string; messages: readonly AiEmployeeMessage[] }
|
|
126
|
+
message: {
|
|
127
|
+
sessionId: string
|
|
128
|
+
message: AiEmployeeMessage
|
|
129
|
+
source: 'local' | 'realtime'
|
|
130
|
+
}
|
|
131
|
+
title: { sessionId: string; title: string }
|
|
132
|
+
'connection-state-change': {
|
|
133
|
+
previous: AiEmployeeConnectionState
|
|
134
|
+
current: AiEmployeeConnectionState
|
|
135
|
+
}
|
|
136
|
+
'processing-state-change': { processing: boolean }
|
|
137
|
+
error: AiEmployeeClientErrorDetail
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface AiEmployeeReconnectOptions {
|
|
141
|
+
initialDelayMs: number
|
|
142
|
+
maximumDelayMs: number
|
|
143
|
+
factor: number
|
|
144
|
+
jitter: number
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type AiEmployeeWebSocketFactory = (url: string) => WebSocket
|
|
148
|
+
|
|
149
|
+
export interface AiEmployeeClientOptions {
|
|
150
|
+
realtimeUrl: string
|
|
151
|
+
aeCode: string
|
|
152
|
+
agentCode: string
|
|
153
|
+
transport: AiEmployeeTransport
|
|
154
|
+
sessionId?: string
|
|
155
|
+
reconnect?: Partial<AiEmployeeReconnectOptions>
|
|
156
|
+
webSocketFactory?: AiEmployeeWebSocketFactory
|
|
157
|
+
requestIdFactory?: () => string
|
|
158
|
+
firstMessageRequestStore?: AiEmployeeFirstMessageRequestStore | null
|
|
159
|
+
/** Primarily useful for deterministic tests. Production heartbeats are never faster than 5s. */
|
|
160
|
+
minimumHeartbeatMs?: number
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface AiEmployeeClientStartResult {
|
|
164
|
+
sessionId: string | null
|
|
165
|
+
messages: readonly AiEmployeeMessage[]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export type AiEmployeeClientListener<K extends keyof AiEmployeeClientEventMap> = (
|
|
169
|
+
detail: AiEmployeeClientEventMap[K],
|
|
170
|
+
) => void
|