@csntgao/uni-base 0.6.3 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -8
- package/CLAUDE.md +10 -8
- package/README.md +27 -21
- 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 +46 -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 +1 -1
- 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 +1 -1
- package/examples/vanilla/main.js +1 -1
- package/examples/vanilla/package.json +3 -3
- package/package.json +12 -13
- package/packages/core/package.json +2 -2
- package/packages/core/src/assistant-actions.ts +120 -0
- package/packages/core/src/customer-support-types.ts +15 -0
- package/packages/core/src/index.ts +8 -0
- package/packages/core/src/types.ts +6 -0
- package/packages/core/tests/assistant-actions.test.ts +76 -0
- package/packages/web-components/package.json +3 -3
- package/packages/web-components/scripts/verify-runtime-build.mjs +1 -0
- package/packages/web-components/src/chat.ts +267 -15
- package/packages/web-components/src/customer-support-chat.ts +1 -1
- package/packages/web-components/src/customer-support-transport.ts +118 -16
- package/packages/web-components/src/index.ts +14 -2
- package/packages/web-components/src/notification-center.ts +1 -1
- package/packages/web-components/src/notification-popover.ts +1 -1
- package/packages/web-components/src/uniplat-base-notification-center-transport.ts +1 -1
- package/packages/web-components/tests/chat.test.ts +418 -10
- package/packages/web-components/tests/customer-support-chat.test.ts +1 -1
- package/packages/web-components/tests/customer-support-transport.test.ts +89 -1
- package/packages/web-components/tests/notification-center.test.ts +1 -1
- package/packages/web-components/tests/notification-popover.test.ts +1 -1
- package/packages/web-components/tsup.config.ts +1 -1
- package/packages/web-components/tsup.runtime.config.ts +1 -1
- package/restart +0 -0
- package/tsconfig.base.json +2 -2
- package/vitest.config.ts +2 -2
- package//344/272/244/346/216/245/346/226/207/346/241/243.md +21 -26
package/AGENTS.md
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
当前交付包括:
|
|
12
12
|
|
|
13
13
|
```text
|
|
14
|
-
packages/core @
|
|
15
|
-
packages/web-components @
|
|
14
|
+
packages/core @csntgao/ai-employee-client
|
|
15
|
+
packages/web-components @csntgao/ai-employee-web-components
|
|
16
16
|
examples/vanilla 原生 HTML / JavaScript 接入示例
|
|
17
17
|
```
|
|
18
18
|
|
|
@@ -97,8 +97,8 @@ pnpm build
|
|
|
97
97
|
|
|
98
98
|
```bash
|
|
99
99
|
pnpm build
|
|
100
|
-
pnpm --filter @
|
|
101
|
-
pnpm --filter @
|
|
100
|
+
pnpm --filter @csntgao/ai-employee-client pack
|
|
101
|
+
pnpm --filter @csntgao/ai-employee-web-components pack
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
该规则适用于 core、Web Components、示例及构建配置等所有代码变更,不因本次修改只涉及其中一个 workspace 而省略另一个发布包。纯文档修改不要求重新打包。向其他项目同步 tarball 时仍须遵守跨项目修改边界,并同步更新调用方依赖声明、锁文件和包校验值。
|
|
@@ -121,7 +121,7 @@ pnpm --filter @uniplat/ai-employee-web-components pack
|
|
|
121
121
|
- serve 目录:`/Users/gaostudio/uni-base/dist/runtime`(即 Web Components 构建产物)。
|
|
122
122
|
- 证书与私钥:`/Users/gaostudio/.uni-base-runtime/10.10.10.131+2.pem` 与 `10.10.10.131+2-key.pem`,由 mkcert 签发的开发证书,SAN 覆盖 `localhost`、`10.10.10.131`、`127.0.0.1`。脚本与证书都放在仓库外的 `~/.uni-base-runtime/`,私钥属凭据不得写入仓库。
|
|
123
123
|
- 客户端需信任的根证书:`/Users/gaostudio/Library/Application Support/mkcert/rootCA.pem`。该证书只在安装了此 rootCA 的机器上被信任;其它机器的调用方需先安装该 rootCA,或改用正式证书。
|
|
124
|
-
- npm registry 当前为 `https://registry.
|
|
124
|
+
- npm registry 当前为 `https://registry.npmjs.org/`,本机已登录 `csntgao`。本仓库的可发布包统一使用 `@csntgao` scope;发布仍需逐次完成 npm 浏览器 2FA,并在远端回读版本后才能声称成功。
|
|
125
125
|
|
|
126
126
|
## Workspace 边界
|
|
127
127
|
|
|
@@ -186,7 +186,7 @@ pnpm --filter @uniplat/ai-employee-web-components pack
|
|
|
186
186
|
|
|
187
187
|
`ai_employee` channel、`session_message`、`session_title_updated`、订阅帧字段以及关闭码都是后端契约,不要仅为前端命名一致性自行修改。
|
|
188
188
|
|
|
189
|
-
人工客服使用同一个 `/api/user-realtime/ws`,但必须单独请求并订阅 `customer_support` channel;不得把客服会话并入 `ai_employee` 状态机。客服 HTTP transport 复用 HR-SaaS `customer_support_api` 的 `user_session_page`、`user_session_create`、`user_message_page`、`user_message_send` 和 `user_mark_read`。首条用户消息必须通过 `user_session_create.initial_message`
|
|
189
|
+
人工客服使用同一个 `/api/user-realtime/ws`,但必须单独请求并订阅 `customer_support` channel;不得把客服会话并入 `ai_employee` 状态机。客服 HTTP transport 复用 HR-SaaS `customer_support_api` 的 `user_session_page`、`user_session_create`、`user_session_handoff_create`、`user_message_page`、`user_message_send` 和 `user_mark_read`。首条用户消息必须通过 `user_session_create.initial_message` 原子创建客服会话,不得先创建空会话再发送。职责越界动作的转接必须通过 `user_session_handoff_create` 原子创建客服会话,只提交来源数字员工会话 ID 和服务端生成的动作 UUID;客户端不得提交 `ae_code`、`agent_code`、组织、用户或角色作为权限依据。点击“转人工”应释放数字员工连接并在同一抽屉切换到 `<uniplat-customer-support-chat>`;不得再向数字员工发送模拟转人工文本,也不得要求 Host 拼装第二个抽屉。未显式配置 `customerSupport` 时,数字员工组件必须复用自身的 `realtimeUrl`、`transport.getAccessToken()` 与同源 `/api/general/project/hr_saas/service/customer_support_api`;只有非标准网关或独立鉴权场景才要求 Host 覆盖。
|
|
190
190
|
|
|
191
191
|
## 安全要求
|
|
192
192
|
|
|
@@ -224,10 +224,12 @@ chat.open(initialMessage?)
|
|
|
224
224
|
|
|
225
225
|
`initialMessage` 是可选字符串;非空时由组件在初始化成功后通过既有消息发送链路自动发送:未配置 `sessionId` 时使用 `createSessionWithFirstMessage()` 原子创建会话并提交首条消息,已配置 `sessionId` 时使用 `sendMessage()` 作为该会话的后续消息。Host 不得代为创建会话或操作组件私有输入状态。未传、空字符串或纯空白时只打开抽屉,保持原有行为。
|
|
226
226
|
|
|
227
|
-
不要把对象、回调或 token 改为 HTML attribute。`sessionId` 可选;传入时直接绑定已有会话,不传时组件只建立本地草稿。transport 必须提供 `createSessionWithFirstMessage()`,由 SDK 生成 UUID `request_id` 并在首条消息时一次性提交创建会话所需的数据;Host 只负责协议字段映射,不得先调用空 Session 创建接口。网络失败、组件重建或同一标签页刷新后,用户重试未改变的同一首条消息时复用原 `request_id`;默认恢复存储只允许包含 UUID 和输入摘要,不得保存消息正文、附件内容、JWT 或后端响应。成功响应中的 `sessionId/messageId/executionId/aeCode/agentCode/conversationType` 是唯一权威标识。`getOrCreateSession()/createSession()` 已从公开 transport 删除,不再保留旧空会话兼容分支。聊天组件初始关闭,Host 只负责配置并调用 `open()` 激活;固定右侧抽屉、标题、关闭按钮、消息区、输入区和转人工入口均由组件自身渲染,关闭时组件自行调用 `close()` 并释放实时连接。Host 不得再包裹重复抽屉或拼装抽屉内部界面。标题左侧会话入口由组件内部切换到原型“我的会话”视图;真实列表复用 transport 的可选 `listSessions()`,选择后由组件调用 core `bindSession()`,可选 `markSessionRead()`
|
|
227
|
+
不要把对象、回调或 token 改为 HTML attribute。`sessionId` 可选;传入时直接绑定已有会话,不传时组件只建立本地草稿。transport 必须提供 `createSessionWithFirstMessage()`,由 SDK 生成 UUID `request_id` 并在首条消息时一次性提交创建会话所需的数据;Host 只负责协议字段映射,不得先调用空 Session 创建接口。网络失败、组件重建或同一标签页刷新后,用户重试未改变的同一首条消息时复用原 `request_id`;默认恢复存储只允许包含 UUID 和输入摘要,不得保存消息正文、附件内容、JWT 或后端响应。成功响应中的 `sessionId/messageId/executionId/aeCode/agentCode/conversationType` 是唯一权威标识。`getOrCreateSession()/createSession()` 已从公开 transport 删除,不再保留旧空会话兼容分支。聊天组件初始关闭,Host 只负责配置并调用 `open()` 激活;固定右侧抽屉、标题、关闭按钮、消息区、输入区和转人工入口均由组件自身渲染,关闭时组件自行调用 `close()` 并释放实时连接。Host 不得再包裹重复抽屉或拼装抽屉内部界面。标题左侧会话入口由组件内部切换到原型“我的会话”视图;真实列表复用 transport 的可选 `listSessions()`,选择后由组件调用 core `bindSession()`,可选 `markSessionRead()` 更新已读状态。已读同步只能使用服务端持久化消息 ID:HTTP 历史 `listMessages()` 与 `listSessions().latestMessageId` 返回的 ID 才可作为 `markSessionRead()` 的游标,实时事件里的 `ws_message_id` 与本地合成 ID 一律不得提交,服务端会以 `INVALID_ARGUMENT` 拒绝。本轮回复结束(`processing` 由真变假)且会话可见时应重新拉取历史,拿到持久化 ID 再同步已读。当前选定会话始终视为已读,在聊天界面和“我的会话”列表中都不显示未读角标,也不计入标题入口的聚合未读数;历史加载完成、列表加载完成、当前会话收到实时消息以及从会话列表返回时,组件应立即清除该会话本地未读并异步同步 Host,失败后在下一次触发时重试。历史消息与列表响应使用不同消息 ID 时,以列表返回的 `latestMessageId` 再次同步,不得让当前会话重新出现未读角标;其他未选定会话的新消息仍应计为未读。
|
|
228
228
|
|
|
229
229
|
数字员工的 `assistant` 回复必须通过共享的安全 Markdown 渲染器展示;用户、系统和错误消息保持普通文本。Markdown 只能生成白名单结构,原始 HTML 必须作为文本转义,链接只允许 HTTP(S) 和 `mailto:`,不得直接注入未净化 HTML。
|
|
230
230
|
|
|
231
|
+
数字员工最终回复尾部可以携带一个平台生成的 `assistant_action/open_customer_support` JSON 结构块。SDK 公共解析器只接受字段集合严格等于 `type/action/transaction-id/trigger/entry`、固定枚举值正确且 `transaction-id` 为 UUID4 的独立尾部 JSON;合法动作从展示文本和会话摘要中移除原始 JSON,并在对应 assistant 消息下渲染“发起人工客服”,非法、不完整、重复或夹在正文中的动作不得执行。动作不得按 `ae_code` 或 `agent_code` 分支。点击时调用客服 transport 的可选 `createHandoffSession({ sourceSessionId, actionTransactionId })`;加载中禁止并发提交,失败后以同一 UUID 重试,成功后使用接口返回的客服会话 ID 切换同一抽屉。动作 UUID 不得进入 DOM、公共事件、URL、日志或安全错误信息。
|
|
232
|
+
|
|
231
233
|
工作台内置应用切换通过 Account Context 的 `workbench` 配置:
|
|
232
234
|
|
|
233
235
|
```ts
|
|
@@ -248,7 +250,7 @@ customerSupportChat.configure({
|
|
|
248
250
|
})
|
|
249
251
|
```
|
|
250
252
|
|
|
251
|
-
`transport`
|
|
253
|
+
`transport` 负责客服会话列表、首条消息原子创建、职责越界交接、历史消息、后续发送、已读同步与 `getAccessToken()`;职责越界交接通过可选 `createHandoffSession()` 暴露,真实 HR-SaaS 对接使用 `createHrSaasCustomerSupportTransport()` 并映射 `user_session_handoff_create`。组件初始关闭,由 Host 或数字员工组件调用 `open()`。组件自身拥有完整抽屉、会话列表、消息区、输入区和关闭行为。公开事件为 `ready`、`message-sent`、`message-received`、`connection-state-change`、`closed` 与 `error`,不得包含 JWT、动作 UUID、transport 或后端原始异常。
|
|
252
254
|
|
|
253
255
|
通知中心通过 JavaScript property 配置:
|
|
254
256
|
|
package/CLAUDE.md
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
当前交付包括:
|
|
12
12
|
|
|
13
13
|
```text
|
|
14
|
-
packages/core @
|
|
15
|
-
packages/web-components @
|
|
14
|
+
packages/core @csntgao/ai-employee-client
|
|
15
|
+
packages/web-components @csntgao/ai-employee-web-components
|
|
16
16
|
examples/vanilla 原生 HTML / JavaScript 接入示例
|
|
17
17
|
```
|
|
18
18
|
|
|
@@ -97,8 +97,8 @@ pnpm build
|
|
|
97
97
|
|
|
98
98
|
```bash
|
|
99
99
|
pnpm build
|
|
100
|
-
pnpm --filter @
|
|
101
|
-
pnpm --filter @
|
|
100
|
+
pnpm --filter @csntgao/ai-employee-client pack
|
|
101
|
+
pnpm --filter @csntgao/ai-employee-web-components pack
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
该规则适用于 core、Web Components、示例及构建配置等所有代码变更,不因本次修改只涉及其中一个 workspace 而省略另一个发布包。纯文档修改不要求重新打包。向其他项目同步 tarball 时仍须遵守跨项目修改边界,并同步更新调用方依赖声明、锁文件和包校验值。
|
|
@@ -121,7 +121,7 @@ pnpm --filter @uniplat/ai-employee-web-components pack
|
|
|
121
121
|
- serve 目录:`/Users/gaostudio/uni-base/dist/runtime`(即 Web Components 构建产物)。
|
|
122
122
|
- 证书与私钥:`/Users/gaostudio/.uni-base-runtime/10.10.10.131+2.pem` 与 `10.10.10.131+2-key.pem`,由 mkcert 签发的开发证书,SAN 覆盖 `localhost`、`10.10.10.131`、`127.0.0.1`。脚本与证书都放在仓库外的 `~/.uni-base-runtime/`,私钥属凭据不得写入仓库。
|
|
123
123
|
- 客户端需信任的根证书:`/Users/gaostudio/Library/Application Support/mkcert/rootCA.pem`。该证书只在安装了此 rootCA 的机器上被信任;其它机器的调用方需先安装该 rootCA,或改用正式证书。
|
|
124
|
-
- npm registry 当前为 `https://registry.
|
|
124
|
+
- npm registry 当前为 `https://registry.npmjs.org/`,本机已登录 `csntgao`。本仓库的可发布包统一使用 `@csntgao` scope;发布仍需逐次完成 npm 浏览器 2FA,并在远端回读版本后才能声称成功。
|
|
125
125
|
|
|
126
126
|
## Workspace 边界
|
|
127
127
|
|
|
@@ -186,7 +186,7 @@ pnpm --filter @uniplat/ai-employee-web-components pack
|
|
|
186
186
|
|
|
187
187
|
`ai_employee` channel、`session_message`、`session_title_updated`、订阅帧字段以及关闭码都是后端契约,不要仅为前端命名一致性自行修改。
|
|
188
188
|
|
|
189
|
-
人工客服使用同一个 `/api/user-realtime/ws`,但必须单独请求并订阅 `customer_support` channel;不得把客服会话并入 `ai_employee` 状态机。客服 HTTP transport 复用 HR-SaaS `customer_support_api` 的 `user_session_page`、`user_session_create`、`user_message_page`、`user_message_send` 和 `user_mark_read`。首条用户消息必须通过 `user_session_create.initial_message`
|
|
189
|
+
人工客服使用同一个 `/api/user-realtime/ws`,但必须单独请求并订阅 `customer_support` channel;不得把客服会话并入 `ai_employee` 状态机。客服 HTTP transport 复用 HR-SaaS `customer_support_api` 的 `user_session_page`、`user_session_create`、`user_session_handoff_create`、`user_message_page`、`user_message_send` 和 `user_mark_read`。首条用户消息必须通过 `user_session_create.initial_message` 原子创建客服会话,不得先创建空会话再发送。职责越界动作的转接必须通过 `user_session_handoff_create` 原子创建客服会话,只提交来源数字员工会话 ID 和服务端生成的动作 UUID;客户端不得提交 `ae_code`、`agent_code`、组织、用户或角色作为权限依据。点击“转人工”应释放数字员工连接并在同一抽屉切换到 `<uniplat-customer-support-chat>`;不得再向数字员工发送模拟转人工文本,也不得要求 Host 拼装第二个抽屉。未显式配置 `customerSupport` 时,数字员工组件必须复用自身的 `realtimeUrl`、`transport.getAccessToken()` 与同源 `/api/general/project/hr_saas/service/customer_support_api`;只有非标准网关或独立鉴权场景才要求 Host 覆盖。
|
|
190
190
|
|
|
191
191
|
## 安全要求
|
|
192
192
|
|
|
@@ -224,10 +224,12 @@ chat.open(initialMessage?)
|
|
|
224
224
|
|
|
225
225
|
`initialMessage` 是可选字符串;非空时由组件在初始化成功后通过既有消息发送链路自动发送:未配置 `sessionId` 时使用 `createSessionWithFirstMessage()` 原子创建会话并提交首条消息,已配置 `sessionId` 时使用 `sendMessage()` 作为该会话的后续消息。Host 不得代为创建会话或操作组件私有输入状态。未传、空字符串或纯空白时只打开抽屉,保持原有行为。
|
|
226
226
|
|
|
227
|
-
不要把对象、回调或 token 改为 HTML attribute。`sessionId` 可选;传入时直接绑定已有会话,不传时组件只建立本地草稿。transport 必须提供 `createSessionWithFirstMessage()`,由 SDK 生成 UUID `request_id` 并在首条消息时一次性提交创建会话所需的数据;Host 只负责协议字段映射,不得先调用空 Session 创建接口。网络失败、组件重建或同一标签页刷新后,用户重试未改变的同一首条消息时复用原 `request_id`;默认恢复存储只允许包含 UUID 和输入摘要,不得保存消息正文、附件内容、JWT 或后端响应。成功响应中的 `sessionId/messageId/executionId/aeCode/agentCode/conversationType` 是唯一权威标识。`getOrCreateSession()/createSession()` 已从公开 transport 删除,不再保留旧空会话兼容分支。聊天组件初始关闭,Host 只负责配置并调用 `open()` 激活;固定右侧抽屉、标题、关闭按钮、消息区、输入区和转人工入口均由组件自身渲染,关闭时组件自行调用 `close()` 并释放实时连接。Host 不得再包裹重复抽屉或拼装抽屉内部界面。标题左侧会话入口由组件内部切换到原型“我的会话”视图;真实列表复用 transport 的可选 `listSessions()`,选择后由组件调用 core `bindSession()`,可选 `markSessionRead()`
|
|
227
|
+
不要把对象、回调或 token 改为 HTML attribute。`sessionId` 可选;传入时直接绑定已有会话,不传时组件只建立本地草稿。transport 必须提供 `createSessionWithFirstMessage()`,由 SDK 生成 UUID `request_id` 并在首条消息时一次性提交创建会话所需的数据;Host 只负责协议字段映射,不得先调用空 Session 创建接口。网络失败、组件重建或同一标签页刷新后,用户重试未改变的同一首条消息时复用原 `request_id`;默认恢复存储只允许包含 UUID 和输入摘要,不得保存消息正文、附件内容、JWT 或后端响应。成功响应中的 `sessionId/messageId/executionId/aeCode/agentCode/conversationType` 是唯一权威标识。`getOrCreateSession()/createSession()` 已从公开 transport 删除,不再保留旧空会话兼容分支。聊天组件初始关闭,Host 只负责配置并调用 `open()` 激活;固定右侧抽屉、标题、关闭按钮、消息区、输入区和转人工入口均由组件自身渲染,关闭时组件自行调用 `close()` 并释放实时连接。Host 不得再包裹重复抽屉或拼装抽屉内部界面。标题左侧会话入口由组件内部切换到原型“我的会话”视图;真实列表复用 transport 的可选 `listSessions()`,选择后由组件调用 core `bindSession()`,可选 `markSessionRead()` 更新已读状态。已读同步只能使用服务端持久化消息 ID:HTTP 历史 `listMessages()` 与 `listSessions().latestMessageId` 返回的 ID 才可作为 `markSessionRead()` 的游标,实时事件里的 `ws_message_id` 与本地合成 ID 一律不得提交,服务端会以 `INVALID_ARGUMENT` 拒绝。本轮回复结束(`processing` 由真变假)且会话可见时应重新拉取历史,拿到持久化 ID 再同步已读。当前选定会话始终视为已读,在聊天界面和“我的会话”列表中都不显示未读角标,也不计入标题入口的聚合未读数;历史加载完成、列表加载完成、当前会话收到实时消息以及从会话列表返回时,组件应立即清除该会话本地未读并异步同步 Host,失败后在下一次触发时重试。历史消息与列表响应使用不同消息 ID 时,以列表返回的 `latestMessageId` 再次同步,不得让当前会话重新出现未读角标;其他未选定会话的新消息仍应计为未读。
|
|
228
228
|
|
|
229
229
|
数字员工的 `assistant` 回复必须通过共享的安全 Markdown 渲染器展示;用户、系统和错误消息保持普通文本。Markdown 只能生成白名单结构,原始 HTML 必须作为文本转义,链接只允许 HTTP(S) 和 `mailto:`,不得直接注入未净化 HTML。
|
|
230
230
|
|
|
231
|
+
数字员工最终回复尾部可以携带一个平台生成的 `assistant_action/open_customer_support` JSON 结构块。SDK 公共解析器只接受字段集合严格等于 `type/action/transaction-id/trigger/entry`、固定枚举值正确且 `transaction-id` 为 UUID4 的独立尾部 JSON;合法动作从展示文本和会话摘要中移除原始 JSON,并在对应 assistant 消息下渲染“发起人工客服”,非法、不完整、重复或夹在正文中的动作不得执行。动作不得按 `ae_code` 或 `agent_code` 分支。点击时调用客服 transport 的可选 `createHandoffSession({ sourceSessionId, actionTransactionId })`;加载中禁止并发提交,失败后以同一 UUID 重试,成功后使用接口返回的客服会话 ID 切换同一抽屉。动作 UUID 不得进入 DOM、公共事件、URL、日志或安全错误信息。
|
|
232
|
+
|
|
231
233
|
工作台内置应用切换通过 Account Context 的 `workbench` 配置:
|
|
232
234
|
|
|
233
235
|
```ts
|
|
@@ -248,7 +250,7 @@ customerSupportChat.configure({
|
|
|
248
250
|
})
|
|
249
251
|
```
|
|
250
252
|
|
|
251
|
-
`transport`
|
|
253
|
+
`transport` 负责客服会话列表、首条消息原子创建、职责越界交接、历史消息、后续发送、已读同步与 `getAccessToken()`;职责越界交接通过可选 `createHandoffSession()` 暴露,真实 HR-SaaS 对接使用 `createHrSaasCustomerSupportTransport()` 并映射 `user_session_handoff_create`。组件初始关闭,由 Host 或数字员工组件调用 `open()`。组件自身拥有完整抽屉、会话列表、消息区、输入区和关闭行为。公开事件为 `ready`、`message-sent`、`message-received`、`connection-state-change`、`closed` 与 `error`,不得包含 JWT、动作 UUID、transport 或后端原始异常。
|
|
252
254
|
|
|
253
255
|
通知中心通过 JavaScript property 配置:
|
|
254
256
|
|
package/README.md
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
## Workspace
|
|
6
6
|
|
|
7
7
|
```text
|
|
8
|
-
packages/core @
|
|
9
|
-
packages/web-components @
|
|
8
|
+
packages/core @csntgao/ai-employee-client
|
|
9
|
+
packages/web-components @csntgao/ai-employee-web-components
|
|
10
10
|
examples/vanilla 无框架接入示例
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
> 包 scope 已从 `@uniplat` 迁移到 `@csntgao`。调用方需要把依赖和 import 分别改为 `@csntgao/ai-employee-client`、`@csntgao/ai-employee-web-components`;`<uniplat-...>` Custom Element 标签、公开类型和后端协议名称保持不变。
|
|
14
|
+
|
|
13
15
|
要求 Node.js 20+ 和 pnpm。常用命令:
|
|
14
16
|
|
|
15
17
|
```bash
|
|
@@ -33,7 +35,7 @@ import {
|
|
|
33
35
|
AiEmployeeClient,
|
|
34
36
|
AiEmployeeTransportError,
|
|
35
37
|
type AiEmployeeTransport,
|
|
36
|
-
} from '@
|
|
38
|
+
} from '@csntgao/ai-employee-client'
|
|
37
39
|
|
|
38
40
|
const transport: AiEmployeeTransport = {
|
|
39
41
|
async createSessionWithFirstMessage(request) {
|
|
@@ -137,7 +139,7 @@ client.destroy()
|
|
|
137
139
|
|
|
138
140
|
### 浏览器运行时发布
|
|
139
141
|
|
|
140
|
-
`pnpm --filter @
|
|
142
|
+
`pnpm --filter @csntgao/ai-employee-web-components build` 除 npm 包的 `dist/` 外,还会生成可由官网运行时加载的独立浏览器 ESM:
|
|
141
143
|
|
|
142
144
|
```text
|
|
143
145
|
dist/runtime/current.json
|
|
@@ -145,7 +147,7 @@ dist/runtime/releases/<version>/index.js
|
|
|
145
147
|
dist/runtime/releases/<version>/index.js.map
|
|
146
148
|
```
|
|
147
149
|
|
|
148
|
-
运行时版本从 Web Components 的 `package.json` 读取。构建会实际导入独立 ESM,校验 Host 必需导出、九个 Custom Element 注册结果,并拒绝任何残留的运行时 import;全部通过后才原子写入本地 `current.json`。独立 ESM 会完整打包 `lit` 和 `@
|
|
150
|
+
运行时版本从 Web Components 的 `package.json` 读取。构建会实际导入独立 ESM,校验 Host 必需导出、九个 Custom Element 注册结果,并拒绝任何残留的运行时 import;全部通过后才原子写入本地 `current.json`。独立 ESM 会完整打包 `lit` 和 `@csntgao/ai-employee-client`。
|
|
149
151
|
|
|
150
152
|
部署时仍须先上传并验证整个版本目录,再原子替换远端 `current.json`;清单使用 `Cache-Control: no-store`,版本目录使用 `Cache-Control: public, max-age=31536000, immutable`。同一版本的文件不得覆盖,回滚只切回先前清单。缓存头、匿名 CORS、CSP 与远端原子替换由静态发布平台负责,不是 JavaScript 构建产物能够代替的配置。
|
|
151
153
|
|
|
@@ -160,7 +162,7 @@ import {
|
|
|
160
162
|
createUniplatBaseNotificationCenterTransport,
|
|
161
163
|
type UniplatNotificationCenter,
|
|
162
164
|
type UniplatNotificationPopover,
|
|
163
|
-
} from '@
|
|
165
|
+
} from '@csntgao/ai-employee-web-components'
|
|
164
166
|
|
|
165
167
|
const transport = createUniplatBaseNotificationCenterTransport({
|
|
166
168
|
serviceBaseUrl,
|
|
@@ -202,7 +204,7 @@ function applyNotificationRealtimeEvent(rawEvent: unknown) {
|
|
|
202
204
|
<uniplat-user-login id="user-login"></uniplat-user-login>
|
|
203
205
|
|
|
204
206
|
<script type="module">
|
|
205
|
-
import { createUniplatBaseUserLoginTransport } from '@
|
|
207
|
+
import { createUniplatBaseUserLoginTransport } from '@csntgao/ai-employee-web-components'
|
|
206
208
|
|
|
207
209
|
const transport = createUniplatBaseUserLoginTransport({
|
|
208
210
|
apiBaseUrl: '/api',
|
|
@@ -281,7 +283,7 @@ transport 可抛出 `OrganizationOnboardingTransportError`,稳定错误码为
|
|
|
281
283
|
UniplatBase 已在现有 `system.org` 服务下提供真实创建能力。Host 使用当前 JWT 对接时,不需要自行拼请求体:
|
|
282
284
|
|
|
283
285
|
```js
|
|
284
|
-
import { createUniplatBaseOrganizationOnboardingTransport } from '@
|
|
286
|
+
import { createUniplatBaseOrganizationOnboardingTransport } from '@csntgao/ai-employee-web-components'
|
|
285
287
|
|
|
286
288
|
accountContext.configure({
|
|
287
289
|
applicationName: 'OfficialWebsite',
|
|
@@ -366,7 +368,7 @@ selector.configure({
|
|
|
366
368
|
S.0 的“弹不弹”判定由纯函数 `resolveOrganizationEntryDecision()` 提供,它不读写宿主存储:
|
|
367
369
|
|
|
368
370
|
```js
|
|
369
|
-
import { resolveOrganizationEntryDecision } from '@
|
|
371
|
+
import { resolveOrganizationEntryDecision } from '@csntgao/ai-employee-web-components'
|
|
370
372
|
|
|
371
373
|
const decision = resolveOrganizationEntryDecision({
|
|
372
374
|
organizations,
|
|
@@ -390,7 +392,7 @@ UniplatBase 的组织投影由 `application.identity/list_available_organization
|
|
|
390
392
|
import {
|
|
391
393
|
createUniplatBaseOrganizationMultiEmployeeSelectorTransport,
|
|
392
394
|
type UniplatOrganizationMultiEmployeeSelectorElement,
|
|
393
|
-
} from '@
|
|
395
|
+
} from '@csntgao/ai-employee-web-components'
|
|
394
396
|
|
|
395
397
|
const selector = document.querySelector<UniplatOrganizationMultiEmployeeSelectorElement>(
|
|
396
398
|
'uniplat-organization-multi-employee-selector',
|
|
@@ -457,7 +459,7 @@ import {
|
|
|
457
459
|
createUniplatBaseAccountContextTransport,
|
|
458
460
|
createUniplatBaseApplicationSwitcherTransport,
|
|
459
461
|
createUniplatBaseOrganizationOnboardingTransport,
|
|
460
|
-
} from '@
|
|
462
|
+
} from '@csntgao/ai-employee-web-components'
|
|
461
463
|
|
|
462
464
|
const applicationName = 'OfficialWebsite'
|
|
463
465
|
const accountContext = document.querySelector('uniplat-account-context')
|
|
@@ -516,7 +518,7 @@ accountContext.addEventListener('organization-selected', async (event) => {
|
|
|
516
518
|
<uniplat-user-menu id="user-menu"></uniplat-user-menu>
|
|
517
519
|
|
|
518
520
|
<script type="module">
|
|
519
|
-
import '@
|
|
521
|
+
import '@csntgao/ai-employee-web-components'
|
|
520
522
|
|
|
521
523
|
const userMenu = document.querySelector('#user-menu')
|
|
522
524
|
userMenu.configure({
|
|
@@ -576,7 +578,7 @@ logout-requested detail: {}
|
|
|
576
578
|
对接 UniplatBase 时使用专用 transport。Application Switcher Host 只提供当前 JWT,不保存目标应用 JWT:
|
|
577
579
|
|
|
578
580
|
```js
|
|
579
|
-
import { createUniplatBaseApplicationSwitcherTransport } from '@
|
|
581
|
+
import { createUniplatBaseApplicationSwitcherTransport } from '@csntgao/ai-employee-web-components'
|
|
580
582
|
|
|
581
583
|
const transport = createUniplatBaseApplicationSwitcherTransport({
|
|
582
584
|
serviceBaseUrl: '/api/general/project/uniplat_base/service',
|
|
@@ -604,7 +606,7 @@ accountContext.addEventListener('application-selected', (event) => {
|
|
|
604
606
|
目标应用需要在启动最早阶段登录 Ticket。目标应用只能取自该前端项目的初始化配置,不能由 URL 覆盖。helper 一旦发现 Ticket,会先从地址栏删除 `uniplat_identity_ticket` 与 `source_system`、保留其他 Fragment 参数,再请求登录接口,并只把校验后的 JWT 交给目标 Host:
|
|
605
607
|
|
|
606
608
|
```js
|
|
607
|
-
import { loginWithUniplatBaseIdentityTicket } from '@
|
|
609
|
+
import { loginWithUniplatBaseIdentityTicket } from '@csntgao/ai-employee-web-components'
|
|
608
610
|
|
|
609
611
|
await loginWithUniplatBaseIdentityTicket({
|
|
610
612
|
serviceBaseUrl: '/api/general/project/uniplat_base/service',
|
|
@@ -622,7 +624,7 @@ await loginWithUniplatBaseIdentityTicket({
|
|
|
622
624
|
对接其他、已经提供扁平 Application Switcher 接口的服务时,可以使用通用 HTTP transport:
|
|
623
625
|
|
|
624
626
|
```js
|
|
625
|
-
import { createHttpApplicationSwitcherTransport } from '@
|
|
627
|
+
import { createHttpApplicationSwitcherTransport } from '@csntgao/ai-employee-web-components'
|
|
626
628
|
|
|
627
629
|
const transport = createHttpApplicationSwitcherTransport({
|
|
628
630
|
endpoint: 'https://hr-saas.example.com/api/application-entries',
|
|
@@ -662,7 +664,7 @@ accountContext.configure({
|
|
|
662
664
|
<uniplat-ai-employee-chat id="chat"></uniplat-ai-employee-chat>
|
|
663
665
|
|
|
664
666
|
<script type="module">
|
|
665
|
-
import '@
|
|
667
|
+
import '@csntgao/ai-employee-web-components'
|
|
666
668
|
|
|
667
669
|
const chat = document.querySelector('#chat')
|
|
668
670
|
chat.configure({
|
|
@@ -680,10 +682,14 @@ accountContext.configure({
|
|
|
680
682
|
|
|
681
683
|
数字员工的 `assistant` 回复按 Markdown 渲染,支持标题、强调、删除线、行内代码、代码块、引用、有序/无序列表、分隔线及安全链接;用户消息、系统消息和错误消息仍按普通文本显示。原始 HTML 不会注入 DOM,非 HTTP(S) 或 `mailto:` 链接不会生成可点击链接。
|
|
682
684
|
|
|
685
|
+
当 KanbanBackend 在最终 assistant 回复尾部附加规范的 `assistant_action/open_customer_support` JSON 时,组件会隐藏原始 JSON,并在该消息下显示“发起人工客服”。解析不依赖 `aeCode` 或 `agentCode`;只有字段集合完整、固定值正确、`transaction-id` 为 UUID4 且动作作为独立尾部 JSON 出现时才可执行。非法、不完整、重复或正文内的动作不会触发接口。SDK 同时导出 `parseAiEmployeeAssistantContent()`,供其他消息视图复用同一安全解析规则。
|
|
686
|
+
|
|
687
|
+
用户点击该动作后,组件调用客服 transport 的 `createHandoffSession()`,请求体只映射为 `source_session_id` 和 `action_transaction_id`。请求期间按钮禁用,失败后可使用同一动作 UUID 重试;成功后组件释放数字员工连接,并使用接口返回的客服会话 ID 在同一抽屉打开人工客服。动作 UUID 不会进入 DOM、公共事件、URL、日志或错误文案。
|
|
688
|
+
|
|
683
689
|
点击“转人工”后,数字员工连接会先释放,再由同一抽屉切换为 `<uniplat-customer-support-chat>`;不会向数字员工发送一条模拟转人工文本,也不要求 Host 创建第二个抽屉。默认情况下,组件复用数字员工的 `realtimeUrl` 和 `transport.getAccessToken()`,并请求同源 `/api/general/project/hr_saas/service/customer_support_api`,因此使用标准网关路径的 Host 无需增加配置。只有客服网关地址或鉴权提供者不同时,才显式传入 `customerSupport` 覆盖默认配置:
|
|
684
690
|
|
|
685
691
|
```ts
|
|
686
|
-
import { createHrSaasCustomerSupportTransport } from '@
|
|
692
|
+
import { createHrSaasCustomerSupportTransport } from '@csntgao/ai-employee-web-components'
|
|
687
693
|
|
|
688
694
|
const customerSupportTransport = createHrSaasCustomerSupportTransport({
|
|
689
695
|
serviceBaseUrl: '/api/general/project/hr_saas/service/customer_support_api',
|
|
@@ -704,12 +710,12 @@ chat.configure({
|
|
|
704
710
|
|
|
705
711
|
### Customer Support Chat
|
|
706
712
|
|
|
707
|
-
`<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` 会话也不会被自动恢复。只有首条消息成功创建会话后,界面才显示“等待客服接入”。
|
|
713
|
+
`<uniplat-customer-support-chat>` 也可以独立使用。它直接复用 HR-SaaS 的 `user_session_page`、`user_session_create`、`user_session_handoff_create`、`user_message_page`、`user_message_send` 和 `user_mark_read`,实时连接订阅 Kanban 的 `customer_support` channel。首条消息通过 `user_session_create.initial_message` 原子创建会话,后续消息使用 `user_message_send`;数字员工职责越界交接由 transport 的可选 `createHandoffSession({ sourceSessionId, actionTransactionId })` 调用 `user_session_handoff_create`。打开抽屉或点击“新对话”只进入本地空白态,不提前创建等待中会话;历史遗留的无消息 `waiting` 会话也不会被自动恢复。只有首条消息成功创建会话后,界面才显示“等待客服接入”。
|
|
708
714
|
|
|
709
715
|
```html
|
|
710
716
|
<uniplat-customer-support-chat id="customer-support"></uniplat-customer-support-chat>
|
|
711
717
|
<script type="module">
|
|
712
|
-
import { createHrSaasCustomerSupportTransport } from '@
|
|
718
|
+
import { createHrSaasCustomerSupportTransport } from '@csntgao/ai-employee-web-components'
|
|
713
719
|
|
|
714
720
|
const customerSupport = document.querySelector('#customer-support')
|
|
715
721
|
customerSupport.configure({
|
|
@@ -753,7 +759,7 @@ uniplat-ai-employee-chat {
|
|
|
753
759
|
}
|
|
754
760
|
```
|
|
755
761
|
|
|
756
|
-
主要 `::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`。
|
|
762
|
+
主要 `::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`、`assistant-action`、`assistant-action-button`、`composer`、`input`、`send-button`、`human-service-button`、`empty`、`loading`、`activity` 和 `error`。
|
|
757
763
|
|
|
758
764
|
## Vue 和 Nuxt
|
|
759
765
|
|
|
@@ -774,7 +780,7 @@ Nuxt v0.1 仅在客户端注册,例如创建 `plugins/ai-employee.client.ts`
|
|
|
774
780
|
|
|
775
781
|
```ts
|
|
776
782
|
export default defineNuxtPlugin(async () => {
|
|
777
|
-
await import('@
|
|
783
|
+
await import('@csntgao/ai-employee-web-components')
|
|
778
784
|
})
|
|
779
785
|
```
|
|
780
786
|
|
|
@@ -36,6 +36,52 @@ Host 不再渲染或包裹以下内容:
|
|
|
36
36
|
|
|
37
37
|
Host 仍负责提供 `realtimeUrl`、`aeCode`、`agentCode` 和 transport。JWT 继续只由 `transport.getAccessToken()` 返回,不得写入组件属性、HTML attribute 或 URL。
|
|
38
38
|
|
|
39
|
+
## 职责越界转人工
|
|
40
|
+
|
|
41
|
+
KanbanBackend 可以在最终 assistant 回复末尾附加以下平台动作:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"type": "assistant_action",
|
|
46
|
+
"action": "open_customer_support",
|
|
47
|
+
"transaction-id": "550e8400-e29b-41d4-a716-446655440000",
|
|
48
|
+
"trigger": "out_of_scope",
|
|
49
|
+
"entry": "help_and_support"
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
组件会在历史消息和实时消息中统一解析这个独立尾部 JSON。合法动作的原始 JSON 不显示,并在对应回复下渲染“发起人工客服”;字段缺失、额外字段、非 UUID4、重复动作或正文中的 JSON 都不会执行。该能力不按数字员工或 Agent 分支。其他消息视图可从 core 或 Web Components 包导入 `parseAiEmployeeAssistantContent()` 复用相同解析规则。
|
|
54
|
+
|
|
55
|
+
标准 `createHrSaasCustomerSupportTransport()` 已实现 `createHandoffSession()`,并将公开参数严格映射为:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"source_session_id": "数字员工会话 ID",
|
|
60
|
+
"action_transaction_id": "动作 transaction-id"
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
点击期间组件会禁止并发提交;失败时显示稳定的安全提示,用户重试会继续使用原动作 UUID;成功时按接口返回的客服会话 ID 释放数字员工连接并切换同一抽屉。Host 无需重建匿名身份,也不得把 `ae_code`、`agent_code`、`org_id`、`user_id` 或角色字段加入转接请求。JWT 继续由原 transport 的 `getAccessToken()` 按请求获取。动作 UUID 不进入 DOM、公共事件、URL、日志或错误文案。
|
|
65
|
+
|
|
66
|
+
如果 Host 覆盖 `customerSupport.transport`,自定义 transport 需要实现可选方法:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
async createHandoffSession({ sourceSessionId, actionTransactionId }) {
|
|
70
|
+
const response = await hostApi.createCustomerSupportHandoff({
|
|
71
|
+
source_session_id: sourceSessionId,
|
|
72
|
+
action_transaction_id: actionTransactionId,
|
|
73
|
+
})
|
|
74
|
+
return {
|
|
75
|
+
session: response.session,
|
|
76
|
+
initialMessage: response.initial_message,
|
|
77
|
+
created: response.created,
|
|
78
|
+
repaired: response.repaired,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
若未显式覆盖客服配置,组件会自动复用数字员工 `realtimeUrl`、`transport.getAccessToken()` 和同源 HR-SaaS 客服网关,因此 QQXBWebsite 的标准接入不需要增加新的 token 或 WebSocket 状态机。
|
|
84
|
+
|
|
39
85
|
## 会话列表
|
|
40
86
|
|
|
41
87
|
标题左侧按钮由组件内部负责,点击后切换为原型中的“我的会话”界面。面板头部的“开启新对话”按钮只切换到本地草稿,不创建后端 Session。用户发送首条消息时,组件才通过 Host transport 原子创建会话并提交首条消息:
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import {
|
|
15
15
|
createUniplatBaseOrganizationSingleEmployeeSelectorTransport,
|
|
16
16
|
type UniplatOrganizationSingleEmployeeSelectorElement,
|
|
17
|
-
} from '@
|
|
17
|
+
} from '@csntgao/ai-employee-web-components'
|
|
18
18
|
|
|
19
19
|
const selector = document.querySelector<UniplatOrganizationSingleEmployeeSelectorElement>(
|
|
20
20
|
'#organization-single-employee-selector',
|
|
@@ -16,7 +16,7 @@ Host 使用 UniBase 的 `current.json` 加载版本化独立 ESM。只要本文
|
|
|
16
16
|
import {
|
|
17
17
|
createUniplatBaseOrganizationMultiEmployeeSelectorTransport,
|
|
18
18
|
type UniplatOrganizationMultiEmployeeSelectorElement,
|
|
19
|
-
} from '@
|
|
19
|
+
} from '@csntgao/ai-employee-web-components'
|
|
20
20
|
|
|
21
21
|
const selector = document.querySelector<UniplatOrganizationMultiEmployeeSelectorElement>(
|
|
22
22
|
'#organization-employee-selector',
|
package/examples/vanilla/main.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
createMockOrganizationOnboardingTransport,
|
|
4
4
|
createUniplatBaseOrganizationOnboardingTransport,
|
|
5
5
|
resolveOrganizationEntryDecision,
|
|
6
|
-
} from '@
|
|
6
|
+
} from '@csntgao/ai-employee-web-components'
|
|
7
7
|
|
|
8
8
|
const config = window.AI_EMPLOYEE_EXAMPLE_CONFIG
|
|
9
9
|
const chat = document.querySelector('#employee-chat')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "@
|
|
2
|
+
"name": "@csntgao/ai-employee-vanilla-example",
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"preview": "vite preview"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@
|
|
13
|
-
"@
|
|
12
|
+
"@csntgao/ai-employee-client": "workspace:*",
|
|
13
|
+
"@csntgao/ai-employee-web-components": "workspace:*"
|
|
14
14
|
}
|
|
15
15
|
}
|
package/package.json
CHANGED
|
@@ -1,21 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@csntgao/uni-base",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"packageManager": "pnpm@11.3.0",
|
|
6
5
|
"publishConfig": {
|
|
7
6
|
"access": "public"
|
|
8
7
|
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "pnpm --filter @uniplat/ai-employee-client build && pnpm --filter @uniplat/ai-employee-web-components build && pnpm --filter @uniplat/ai-employee-vanilla-example build",
|
|
11
|
-
"dev": "pnpm --filter @uniplat/ai-employee-client build && pnpm --filter @uniplat/ai-employee-web-components build && pnpm --filter @uniplat/ai-employee-vanilla-example dev",
|
|
12
|
-
"lint": "eslint . --max-warnings 0",
|
|
13
|
-
"format": "prettier --write .",
|
|
14
|
-
"format:check": "prettier --check .",
|
|
15
|
-
"test": "vitest run",
|
|
16
|
-
"test:watch": "vitest",
|
|
17
|
-
"typecheck": "pnpm --filter @uniplat/ai-employee-client typecheck && pnpm --filter @uniplat/ai-employee-web-components typecheck"
|
|
18
|
-
},
|
|
19
8
|
"devDependencies": {
|
|
20
9
|
"@eslint/js": "^9.32.0",
|
|
21
10
|
"eslint": "^9.32.0",
|
|
@@ -27,5 +16,15 @@
|
|
|
27
16
|
"typescript-eslint": "^8.39.0",
|
|
28
17
|
"vite": "^7.1.0",
|
|
29
18
|
"vitest": "^3.2.4"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "pnpm --filter @csntgao/ai-employee-client build && pnpm --filter @csntgao/ai-employee-web-components build && pnpm --filter @csntgao/ai-employee-vanilla-example build",
|
|
22
|
+
"dev": "pnpm --filter @csntgao/ai-employee-client build && pnpm --filter @csntgao/ai-employee-web-components build && pnpm --filter @csntgao/ai-employee-vanilla-example dev",
|
|
23
|
+
"lint": "eslint . --max-warnings 0",
|
|
24
|
+
"format": "prettier --write .",
|
|
25
|
+
"format:check": "prettier --check .",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest",
|
|
28
|
+
"typecheck": "pnpm --filter @csntgao/ai-employee-client typecheck && pnpm --filter @csntgao/ai-employee-web-components typecheck"
|
|
30
29
|
}
|
|
31
|
-
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export interface OpenCustomerSupportAssistantAction {
|
|
2
|
+
type: 'assistant_action'
|
|
3
|
+
action: 'open_customer_support'
|
|
4
|
+
transactionId: string
|
|
5
|
+
trigger: 'out_of_scope'
|
|
6
|
+
entry: 'help_and_support'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type AiEmployeeAssistantAction = OpenCustomerSupportAssistantAction
|
|
10
|
+
|
|
11
|
+
export interface ParsedAiEmployeeAssistantContent {
|
|
12
|
+
content: string
|
|
13
|
+
actions: readonly AiEmployeeAssistantAction[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface StructuredJsonCandidate {
|
|
17
|
+
content: string
|
|
18
|
+
json: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ACTION_KEYS = ['action', 'entry', 'transaction-id', 'trigger', 'type'] as const
|
|
22
|
+
const OPEN_CUSTOMER_SUPPORT_MARKER = /"action"\s*:\s*"open_customer_support"/g
|
|
23
|
+
const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parses a trusted platform action from the final standalone JSON block of an
|
|
27
|
+
* assistant response. Invalid or ambiguous input is returned unchanged and is
|
|
28
|
+
* never made executable.
|
|
29
|
+
*/
|
|
30
|
+
export function parseAiEmployeeAssistantContent(content: string): ParsedAiEmployeeAssistantContent {
|
|
31
|
+
const source = typeof content === 'string' ? content : ''
|
|
32
|
+
const candidate = trailingStructuredJson(source)
|
|
33
|
+
if (!candidate || markerCount(source) !== 1) return unchanged(source)
|
|
34
|
+
|
|
35
|
+
let value: unknown
|
|
36
|
+
try {
|
|
37
|
+
value = JSON.parse(candidate.json)
|
|
38
|
+
} catch {
|
|
39
|
+
return unchanged(source)
|
|
40
|
+
}
|
|
41
|
+
const action = parseOpenCustomerSupportAction(value)
|
|
42
|
+
if (!action) return unchanged(source)
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
content: candidate.content,
|
|
45
|
+
actions: Object.freeze([action]),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isUuidV4(value: unknown): value is string {
|
|
50
|
+
return typeof value === 'string' && UUID_V4_PATTERN.test(value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function trailingStructuredJson(content: string): StructuredJsonCandidate | null {
|
|
54
|
+
const withoutTrailingWhitespace = content.trimEnd()
|
|
55
|
+
const fenced = withoutTrailingWhitespace.match(
|
|
56
|
+
/(?:^|\r?\n)[\t ]*```json[\t ]*\r?\n([\s\S]*?)\r?\n[\t ]*```$/i,
|
|
57
|
+
)
|
|
58
|
+
if (fenced?.index !== undefined) {
|
|
59
|
+
return {
|
|
60
|
+
content: withoutTrailingWhitespace.slice(0, fenced.index).trimEnd(),
|
|
61
|
+
json: fenced[1]?.trim() ?? '',
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const starts = [...withoutTrailingWhitespace.matchAll(/(?:^|\r?\n)[\t ]*(\{)/g)]
|
|
66
|
+
for (let index = starts.length - 1; index >= 0; index -= 1) {
|
|
67
|
+
const match = starts[index]
|
|
68
|
+
const objectOffset =
|
|
69
|
+
match?.index === undefined ? -1 : match.index + (match[0]?.indexOf('{') ?? -1)
|
|
70
|
+
if (objectOffset < 0) continue
|
|
71
|
+
const json = withoutTrailingWhitespace.slice(objectOffset).trim()
|
|
72
|
+
try {
|
|
73
|
+
JSON.parse(json)
|
|
74
|
+
return {
|
|
75
|
+
content: withoutTrailingWhitespace.slice(0, match?.index ?? 0).trimEnd(),
|
|
76
|
+
json,
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Try an earlier line-start object. Nested or malformed trailing JSON is
|
|
80
|
+
// never accepted merely because it contains an action-looking fragment.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseOpenCustomerSupportAction(value: unknown): OpenCustomerSupportAssistantAction | null {
|
|
87
|
+
if (!isRecord(value)) return null
|
|
88
|
+
const keys = Object.keys(value).sort()
|
|
89
|
+
if (keys.length !== ACTION_KEYS.length || keys.some((key, index) => key !== ACTION_KEYS[index])) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
value.type !== 'assistant_action' ||
|
|
94
|
+
value.action !== 'open_customer_support' ||
|
|
95
|
+
!isUuidV4(value['transaction-id']) ||
|
|
96
|
+
value.trigger !== 'out_of_scope' ||
|
|
97
|
+
value.entry !== 'help_and_support'
|
|
98
|
+
) {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
type: 'assistant_action',
|
|
103
|
+
action: 'open_customer_support',
|
|
104
|
+
transactionId: value['transaction-id'],
|
|
105
|
+
trigger: 'out_of_scope',
|
|
106
|
+
entry: 'help_and_support',
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function markerCount(value: string): number {
|
|
111
|
+
return [...value.matchAll(OPEN_CUSTOMER_SUPPORT_MARKER)].length
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function unchanged(content: string): ParsedAiEmployeeAssistantContent {
|
|
115
|
+
return Object.freeze({ content, actions: Object.freeze([]) })
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
119
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
120
|
+
}
|