@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.
Files changed (35) hide show
  1. package/AGENTS.md +10 -8
  2. package/CLAUDE.md +10 -8
  3. package/README.md +27 -21
  4. 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
  5. 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
  6. 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
  7. package/examples/vanilla/main.js +1 -1
  8. package/examples/vanilla/package.json +3 -3
  9. package/package.json +12 -13
  10. package/packages/core/package.json +2 -2
  11. package/packages/core/src/assistant-actions.ts +120 -0
  12. package/packages/core/src/customer-support-types.ts +15 -0
  13. package/packages/core/src/index.ts +8 -0
  14. package/packages/core/src/types.ts +6 -0
  15. package/packages/core/tests/assistant-actions.test.ts +76 -0
  16. package/packages/web-components/package.json +3 -3
  17. package/packages/web-components/scripts/verify-runtime-build.mjs +1 -0
  18. package/packages/web-components/src/chat.ts +267 -15
  19. package/packages/web-components/src/customer-support-chat.ts +1 -1
  20. package/packages/web-components/src/customer-support-transport.ts +118 -16
  21. package/packages/web-components/src/index.ts +14 -2
  22. package/packages/web-components/src/notification-center.ts +1 -1
  23. package/packages/web-components/src/notification-popover.ts +1 -1
  24. package/packages/web-components/src/uniplat-base-notification-center-transport.ts +1 -1
  25. package/packages/web-components/tests/chat.test.ts +418 -10
  26. package/packages/web-components/tests/customer-support-chat.test.ts +1 -1
  27. package/packages/web-components/tests/customer-support-transport.test.ts +89 -1
  28. package/packages/web-components/tests/notification-center.test.ts +1 -1
  29. package/packages/web-components/tests/notification-popover.test.ts +1 -1
  30. package/packages/web-components/tsup.config.ts +1 -1
  31. package/packages/web-components/tsup.runtime.config.ts +1 -1
  32. package/restart +0 -0
  33. package/tsconfig.base.json +2 -2
  34. package/vitest.config.ts +2 -2
  35. 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 @uniplat/ai-employee-client
15
- packages/web-components @uniplat/ai-employee-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 @uniplat/ai-employee-client pack
101
- pnpm --filter @uniplat/ai-employee-web-components pack
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.npmmirror.com` 且本机未登录;该镜像通常只读。npm 发布在用户配置可写 registry 并完成登录前一律视为外部阻塞,不得声称 npm 发布成功。
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` 原子创建客服会话,不得先创建空会话再发送。点击“转人工”应释放数字员工连接并在同一抽屉切换到 `<uniplat-customer-support-chat>`;不得再向数字员工发送模拟转人工文本,也不得要求 Host 拼装第二个抽屉。未显式配置 `customerSupport` 时,数字员工组件必须复用自身的 `realtimeUrl`、`transport.getAccessToken()` 与同源 `/api/general/project/hr_saas/service/customer_support_api`;只有非标准网关或独立鉴权场景才要求 Host 覆盖。
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()` 更新已读状态。当前选定会话始终视为已读,在聊天界面和“我的会话”列表中都不显示未读角标,也不计入标题入口的聚合未读数;历史加载完成、列表加载完成、当前会话收到实时消息以及从会话列表返回时,组件应立即清除该会话本地未读并异步同步 Host,失败后在下一次触发时重试。历史消息与列表响应使用不同消息 ID 时,以列表返回的 `latestMessageId` 再次同步,不得让当前会话重新出现未读角标;其他未选定会话的新消息仍应计为未读。
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` 负责客服会话列表、首条消息原子创建、历史消息、后续发送、已读同步与 `getAccessToken()`;真实 HR-SaaS 对接使用 `createHrSaasCustomerSupportTransport()`。组件初始关闭,由 Host 或数字员工组件调用 `open()`。组件自身拥有完整抽屉、会话列表、消息区、输入区和关闭行为。公开事件为 `ready`、`message-sent`、`message-received`、`connection-state-change`、`closed` 与 `error`,不得包含 JWT、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 @uniplat/ai-employee-client
15
- packages/web-components @uniplat/ai-employee-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 @uniplat/ai-employee-client pack
101
- pnpm --filter @uniplat/ai-employee-web-components pack
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.npmmirror.com` 且本机未登录;该镜像通常只读。npm 发布在用户配置可写 registry 并完成登录前一律视为外部阻塞,不得声称 npm 发布成功。
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` 原子创建客服会话,不得先创建空会话再发送。点击“转人工”应释放数字员工连接并在同一抽屉切换到 `<uniplat-customer-support-chat>`;不得再向数字员工发送模拟转人工文本,也不得要求 Host 拼装第二个抽屉。未显式配置 `customerSupport` 时,数字员工组件必须复用自身的 `realtimeUrl`、`transport.getAccessToken()` 与同源 `/api/general/project/hr_saas/service/customer_support_api`;只有非标准网关或独立鉴权场景才要求 Host 覆盖。
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()` 更新已读状态。当前选定会话始终视为已读,在聊天界面和“我的会话”列表中都不显示未读角标,也不计入标题入口的聚合未读数;历史加载完成、列表加载完成、当前会话收到实时消息以及从会话列表返回时,组件应立即清除该会话本地未读并异步同步 Host,失败后在下一次触发时重试。历史消息与列表响应使用不同消息 ID 时,以列表返回的 `latestMessageId` 再次同步,不得让当前会话重新出现未读角标;其他未选定会话的新消息仍应计为未读。
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` 负责客服会话列表、首条消息原子创建、历史消息、后续发送、已读同步与 `getAccessToken()`;真实 HR-SaaS 对接使用 `createHrSaasCustomerSupportTransport()`。组件初始关闭,由 Host 或数字员工组件调用 `open()`。组件自身拥有完整抽屉、会话列表、消息区、输入区和关闭行为。公开事件为 `ready`、`message-sent`、`message-received`、`connection-state-change`、`closed` 与 `error`,不得包含 JWT、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 @uniplat/ai-employee-client
9
- packages/web-components @uniplat/ai-employee-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 '@uniplat/ai-employee-client'
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 @uniplat/ai-employee-web-components build` 除 npm 包的 `dist/` 外,还会生成可由官网运行时加载的独立浏览器 ESM:
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` 和 `@uniplat/ai-employee-client`。
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 '@uniplat/ai-employee-web-components'
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 '@uniplat/ai-employee-web-components'
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 '@uniplat/ai-employee-web-components'
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 '@uniplat/ai-employee-web-components'
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 '@uniplat/ai-employee-web-components'
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 '@uniplat/ai-employee-web-components'
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 '@uniplat/ai-employee-web-components'
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 '@uniplat/ai-employee-web-components'
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 '@uniplat/ai-employee-web-components'
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 '@uniplat/ai-employee-web-components'
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 '@uniplat/ai-employee-web-components'
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 '@uniplat/ai-employee-web-components'
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 '@uniplat/ai-employee-web-components'
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('@uniplat/ai-employee-web-components')
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 '@uniplat/ai-employee-web-components'
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 '@uniplat/ai-employee-web-components'
19
+ } from '@csntgao/ai-employee-web-components'
20
20
 
21
21
  const selector = document.querySelector<UniplatOrganizationMultiEmployeeSelectorElement>(
22
22
  '#organization-employee-selector',
@@ -3,7 +3,7 @@ import {
3
3
  createMockOrganizationOnboardingTransport,
4
4
  createUniplatBaseOrganizationOnboardingTransport,
5
5
  resolveOrganizationEntryDecision,
6
- } from '@uniplat/ai-employee-web-components'
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": "@uniplat/ai-employee-vanilla-example",
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
- "@uniplat/ai-employee-client": "workspace:*",
13
- "@uniplat/ai-employee-web-components": "workspace:*"
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",
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
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
- "name": "@uniplat/ai-employee-client",
3
- "version": "0.14.1",
2
+ "name": "@csntgao/ai-employee-client",
3
+ "version": "0.14.3",
4
4
  "description": "Browser client for Uniplat AI Employee sessions and realtime messages",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -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
+ }