@anker-in/campaign-ui 0.3.2 → 0.3.4

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 (229) hide show
  1. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.d.ts +21 -1
  2. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js +1 -1
  3. package/dist/cjs/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  4. package/dist/cjs/components/LiveChatWidget/api/chat.d.ts +23 -2
  5. package/dist/cjs/components/LiveChatWidget/api/chat.js +2 -2
  6. package/dist/cjs/components/LiveChatWidget/api/chat.js.map +3 -3
  7. package/dist/cjs/components/LiveChatWidget/components/ChatHeader.js +1 -1
  8. package/dist/cjs/components/LiveChatWidget/components/ChatHeader.js.map +2 -2
  9. package/dist/cjs/components/LiveChatWidget/components/ChatInput.d.ts +5 -0
  10. package/dist/cjs/components/LiveChatWidget/components/ChatInput.js +1 -1
  11. package/dist/cjs/components/LiveChatWidget/components/ChatInput.js.map +3 -3
  12. package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js +2 -2
  13. package/dist/cjs/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
  14. package/dist/cjs/components/LiveChatWidget/components/ChatWindow.d.ts +5 -0
  15. package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js +1 -1
  16. package/dist/cjs/components/LiveChatWidget/components/ChatWindow.js.map +3 -3
  17. package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.d.ts +51 -0
  18. package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.js +33 -0
  19. package/dist/cjs/components/LiveChatWidget/components/ComplianceDialog.js.map +7 -0
  20. package/dist/cjs/components/LiveChatWidget/components/MessageContent/CartCard.js +1 -1
  21. package/dist/cjs/components/LiveChatWidget/components/MessageContent/CartCard.js.map +3 -3
  22. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ErrorBlock.js +1 -1
  23. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ErrorBlock.js.map +2 -2
  24. package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
  25. package/dist/cjs/components/LiveChatWidget/components/MessageContent/FAQList.js.map +3 -3
  26. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +2 -2
  27. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +3 -3
  28. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +17 -24
  29. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -4
  30. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
  31. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +7 -1
  32. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
  33. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
  34. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
  35. package/dist/cjs/components/LiveChatWidget/components/MessageContent/ProductList.js.map +3 -3
  36. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.d.ts +4 -1
  37. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.js +1 -1
  38. package/dist/cjs/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +3 -3
  39. package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
  40. package/dist/cjs/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
  41. package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
  42. package/dist/cjs/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +3 -3
  43. package/dist/cjs/components/LiveChatWidget/components/MessageContent.js +1 -1
  44. package/dist/cjs/components/LiveChatWidget/components/MessageContent.js.map +2 -2
  45. package/dist/cjs/components/LiveChatWidget/components/MessageList.js +2 -2
  46. package/dist/cjs/components/LiveChatWidget/components/MessageList.js.map +2 -2
  47. package/dist/cjs/components/LiveChatWidget/constants.d.ts +5 -0
  48. package/dist/cjs/components/LiveChatWidget/constants.js +1 -1
  49. package/dist/cjs/components/LiveChatWidget/constants.js.map +3 -3
  50. package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.d.ts +9 -0
  51. package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.js +1 -1
  52. package/dist/cjs/components/LiveChatWidget/hooks/useChatAPI.js.map +3 -3
  53. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.d.ts +35 -2
  54. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js +1 -1
  55. package/dist/cjs/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
  56. package/dist/cjs/components/LiveChatWidget/index.d.ts +1 -1
  57. package/dist/cjs/components/LiveChatWidget/index.js +1 -1
  58. package/dist/cjs/components/LiveChatWidget/index.js.map +2 -2
  59. package/dist/cjs/components/LiveChatWidget/types.d.ts +212 -3
  60. package/dist/cjs/components/LiveChatWidget/types.js +1 -1
  61. package/dist/cjs/components/LiveChatWidget/types.js.map +1 -1
  62. package/dist/cjs/components/LiveChatWidget/utils/fetcher.d.ts +42 -0
  63. package/dist/cjs/components/LiveChatWidget/utils/fetcher.js +2 -0
  64. package/dist/cjs/components/LiveChatWidget/utils/fetcher.js.map +7 -0
  65. package/dist/cjs/components/chat/markdown.js +1 -1
  66. package/dist/cjs/components/chat/markdown.js.map +2 -2
  67. package/dist/cjs/components/credits/creditsBanner/index.js +2 -2
  68. package/dist/cjs/components/credits/creditsBanner/index.js.map +2 -2
  69. package/dist/cjs/components/index.d.ts +2 -0
  70. package/dist/cjs/components/index.js +1 -1
  71. package/dist/cjs/components/index.js.map +3 -3
  72. package/dist/cjs/stories/LiveChatWidget.stories.d.ts +1 -79
  73. package/dist/cjs/stories/LiveChatWidget.stories.js +8 -47
  74. package/dist/cjs/stories/LiveChatWidget.stories.js.map +3 -3
  75. package/dist/esm/components/LiveChatWidget/LiveChatWidget.d.ts +21 -1
  76. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js +1 -1
  77. package/dist/esm/components/LiveChatWidget/LiveChatWidget.js.map +3 -3
  78. package/dist/esm/components/LiveChatWidget/api/chat.d.ts +23 -2
  79. package/dist/esm/components/LiveChatWidget/api/chat.js +2 -2
  80. package/dist/esm/components/LiveChatWidget/api/chat.js.map +3 -3
  81. package/dist/esm/components/LiveChatWidget/components/ChatHeader.js +1 -1
  82. package/dist/esm/components/LiveChatWidget/components/ChatHeader.js.map +2 -2
  83. package/dist/esm/components/LiveChatWidget/components/ChatInput.d.ts +5 -0
  84. package/dist/esm/components/LiveChatWidget/components/ChatInput.js +1 -1
  85. package/dist/esm/components/LiveChatWidget/components/ChatInput.js.map +3 -3
  86. package/dist/esm/components/LiveChatWidget/components/ChatMessage.js +2 -2
  87. package/dist/esm/components/LiveChatWidget/components/ChatMessage.js.map +3 -3
  88. package/dist/esm/components/LiveChatWidget/components/ChatWindow.d.ts +5 -0
  89. package/dist/esm/components/LiveChatWidget/components/ChatWindow.js +1 -1
  90. package/dist/esm/components/LiveChatWidget/components/ChatWindow.js.map +3 -3
  91. package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.d.ts +51 -0
  92. package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.js +33 -0
  93. package/dist/esm/components/LiveChatWidget/components/ComplianceDialog.js.map +7 -0
  94. package/dist/esm/components/LiveChatWidget/components/MessageContent/CartCard.js +1 -1
  95. package/dist/esm/components/LiveChatWidget/components/MessageContent/CartCard.js.map +3 -3
  96. package/dist/esm/components/LiveChatWidget/components/MessageContent/ErrorBlock.js +1 -1
  97. package/dist/esm/components/LiveChatWidget/components/MessageContent/ErrorBlock.js.map +2 -2
  98. package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js +1 -1
  99. package/dist/esm/components/LiveChatWidget/components/MessageContent/FAQList.js.map +3 -3
  100. package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js +2 -2
  101. package/dist/esm/components/LiveChatWidget/components/MessageContent/PolicyBlock.js.map +3 -3
  102. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.d.ts +17 -24
  103. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js +1 -4
  104. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductCard.js.map +3 -3
  105. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.d.ts +7 -1
  106. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js +1 -1
  107. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductComparison.js.map +3 -3
  108. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js +1 -1
  109. package/dist/esm/components/LiveChatWidget/components/MessageContent/ProductList.js.map +3 -3
  110. package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.d.ts +4 -1
  111. package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.js +1 -1
  112. package/dist/esm/components/LiveChatWidget/components/MessageContent/PromotionList.js.map +3 -3
  113. package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js +1 -1
  114. package/dist/esm/components/LiveChatWidget/components/MessageContent/QuickReplies.js.map +2 -2
  115. package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js +1 -1
  116. package/dist/esm/components/LiveChatWidget/components/MessageContent/TextBlock.js.map +3 -3
  117. package/dist/esm/components/LiveChatWidget/components/MessageContent.js +1 -1
  118. package/dist/esm/components/LiveChatWidget/components/MessageContent.js.map +2 -2
  119. package/dist/esm/components/LiveChatWidget/components/MessageList.js +2 -2
  120. package/dist/esm/components/LiveChatWidget/components/MessageList.js.map +2 -2
  121. package/dist/esm/components/LiveChatWidget/constants.d.ts +5 -0
  122. package/dist/esm/components/LiveChatWidget/constants.js +1 -1
  123. package/dist/esm/components/LiveChatWidget/constants.js.map +3 -3
  124. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.d.ts +9 -0
  125. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js +1 -1
  126. package/dist/esm/components/LiveChatWidget/hooks/useChatAPI.js.map +3 -3
  127. package/dist/esm/components/LiveChatWidget/hooks/useChatState.d.ts +35 -2
  128. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js +1 -1
  129. package/dist/esm/components/LiveChatWidget/hooks/useChatState.js.map +3 -3
  130. package/dist/esm/components/LiveChatWidget/index.d.ts +1 -1
  131. package/dist/esm/components/LiveChatWidget/index.js +1 -1
  132. package/dist/esm/components/LiveChatWidget/index.js.map +2 -2
  133. package/dist/esm/components/LiveChatWidget/types.d.ts +212 -3
  134. package/dist/esm/components/LiveChatWidget/utils/fetcher.d.ts +42 -0
  135. package/dist/esm/components/LiveChatWidget/utils/fetcher.js +2 -0
  136. package/dist/esm/components/LiveChatWidget/utils/fetcher.js.map +7 -0
  137. package/dist/esm/components/chat/markdown.js +1 -1
  138. package/dist/esm/components/chat/markdown.js.map +2 -2
  139. package/dist/esm/components/credits/creditsBanner/index.js +2 -2
  140. package/dist/esm/components/credits/creditsBanner/index.js.map +2 -2
  141. package/dist/esm/components/index.d.ts +2 -0
  142. package/dist/esm/components/index.js +1 -1
  143. package/dist/esm/components/index.js.map +3 -3
  144. package/dist/esm/stories/LiveChatWidget.stories.d.ts +1 -79
  145. package/dist/esm/stories/LiveChatWidget.stories.js +8 -47
  146. package/dist/esm/stories/LiveChatWidget.stories.js.map +3 -3
  147. package/dist/index.d.mts +1305 -0
  148. package/dist/index.d.ts +1305 -0
  149. package/dist/index.js +26656 -0
  150. package/dist/index.js.map +1 -0
  151. package/dist/index.mjs +26641 -0
  152. package/dist/index.mjs.map +1 -0
  153. package/package.json +8 -1
  154. package/src/components/LiveChatWidget/LiveChatWidget.tsx +887 -0
  155. package/src/components/LiveChatWidget/api/chat.ts +175 -0
  156. package/src/components/LiveChatWidget/components/ChatBubble.tsx +152 -0
  157. package/src/components/LiveChatWidget/components/ChatHeader.tsx +150 -0
  158. package/src/components/LiveChatWidget/components/ChatInput.tsx +253 -0
  159. package/src/components/LiveChatWidget/components/ChatMessage.tsx +190 -0
  160. package/src/components/LiveChatWidget/components/ChatWindow.tsx +363 -0
  161. package/src/components/LiveChatWidget/components/ComplianceDialog.tsx +216 -0
  162. package/src/components/LiveChatWidget/components/MessageContent/CartCard.tsx +202 -0
  163. package/src/components/LiveChatWidget/components/MessageContent/ErrorBlock.tsx +75 -0
  164. package/src/components/LiveChatWidget/components/MessageContent/FAQList.tsx +128 -0
  165. package/src/components/LiveChatWidget/components/MessageContent/PolicyBlock.tsx +152 -0
  166. package/src/components/LiveChatWidget/components/MessageContent/ProductCard.tsx +227 -0
  167. package/src/components/LiveChatWidget/components/MessageContent/ProductComparison.tsx +377 -0
  168. package/src/components/LiveChatWidget/components/MessageContent/ProductList.tsx +293 -0
  169. package/src/components/LiveChatWidget/components/MessageContent/PromotionList.tsx +170 -0
  170. package/src/components/LiveChatWidget/components/MessageContent/QuickReplies.tsx +91 -0
  171. package/src/components/LiveChatWidget/components/MessageContent/TextBlock.tsx +110 -0
  172. package/src/components/LiveChatWidget/components/MessageContent/ThinkingBlock.tsx +53 -0
  173. package/src/components/LiveChatWidget/components/MessageContent/index.ts +16 -0
  174. package/src/components/LiveChatWidget/components/MessageContent.tsx +113 -0
  175. package/src/components/LiveChatWidget/components/MessageList.tsx +261 -0
  176. package/src/components/LiveChatWidget/components/ScrollAnchor.tsx +75 -0
  177. package/src/components/LiveChatWidget/constants.ts +36 -0
  178. package/src/components/LiveChatWidget/hooks/useChatAPI.ts +146 -0
  179. package/src/components/LiveChatWidget/hooks/useChatState.ts +1090 -0
  180. package/src/components/LiveChatWidget/hooks/useSession.ts +123 -0
  181. package/src/components/LiveChatWidget/index.tsx +63 -0
  182. package/src/components/LiveChatWidget/types.ts +1011 -0
  183. package/src/components/LiveChatWidget/utils/cartTransformers.ts +72 -0
  184. package/src/components/LiveChatWidget/utils/fetcher.ts +131 -0
  185. package/src/components/LiveChatWidget/utils/messageRenderers.ts +120 -0
  186. package/src/components/LiveChatWidget/utils/productTransformers.ts +149 -0
  187. package/src/components/LiveChatWidget/utils/userId.ts +140 -0
  188. package/src/components/LiveChatWidget/utils/validation.ts +99 -0
  189. package/src/components/chat/markdown.tsx +1 -1
  190. package/src/components/credits/creditsBanner/index.tsx +5 -5
  191. package/src/components/index.ts +23 -0
  192. package/src/stories/LiveChatWidget.stories.tsx +322 -0
  193. package/src/styles/livechat.css +317 -0
  194. package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.d.ts +0 -7
  195. package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.js +0 -2
  196. package/dist/cjs/components/credits/context/hooks/useFunctionMemberPrice.js.map +0 -7
  197. package/dist/cjs/components/credits/context/utils/atobID.d.ts +0 -1
  198. package/dist/cjs/components/credits/context/utils/atobID.js +0 -2
  199. package/dist/cjs/components/credits/context/utils/atobID.js.map +0 -7
  200. package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.d.ts +0 -5
  201. package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.js +0 -2
  202. package/dist/cjs/components/credits/context/utils/functionDiscountCalculate.js.map +0 -7
  203. package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.d.ts +0 -8
  204. package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.js +0 -2
  205. package/dist/cjs/components/credits/context/utils/getFunctionMemberPrice.js.map +0 -7
  206. package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.d.ts +0 -9
  207. package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js +0 -2
  208. package/dist/cjs/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js.map +0 -7
  209. package/dist/cjs/components/credits/context/utils/variantGetCoupon.d.ts +0 -6
  210. package/dist/cjs/components/credits/context/utils/variantGetCoupon.js +0 -2
  211. package/dist/cjs/components/credits/context/utils/variantGetCoupon.js.map +0 -7
  212. package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.d.ts +0 -7
  213. package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.js +0 -2
  214. package/dist/esm/components/credits/context/hooks/useFunctionMemberPrice.js.map +0 -7
  215. package/dist/esm/components/credits/context/utils/atobID.d.ts +0 -1
  216. package/dist/esm/components/credits/context/utils/atobID.js +0 -2
  217. package/dist/esm/components/credits/context/utils/atobID.js.map +0 -7
  218. package/dist/esm/components/credits/context/utils/functionDiscountCalculate.d.ts +0 -5
  219. package/dist/esm/components/credits/context/utils/functionDiscountCalculate.js +0 -2
  220. package/dist/esm/components/credits/context/utils/functionDiscountCalculate.js.map +0 -7
  221. package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.d.ts +0 -8
  222. package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.js +0 -2
  223. package/dist/esm/components/credits/context/utils/getFunctionMemberPrice.js.map +0 -7
  224. package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.d.ts +0 -9
  225. package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js +0 -2
  226. package/dist/esm/components/credits/context/utils/getFunctionMemberPriceDiscountConfig.js.map +0 -7
  227. package/dist/esm/components/credits/context/utils/variantGetCoupon.d.ts +0 -6
  228. package/dist/esm/components/credits/context/utils/variantGetCoupon.js +0 -2
  229. package/dist/esm/components/credits/context/utils/variantGetCoupon.js.map +0 -7
@@ -0,0 +1,1090 @@
1
+ /**
2
+ * 聊天状态管理 Hook
3
+ * 管理消息列表、窗口状态、输入框状态、流式消息累积
4
+ * 基于 specs/livechat-widget/plan.md 的状态管理策略
5
+ */
6
+
7
+ import { useState, useCallback, useRef, useEffect } from 'react'
8
+ import type { Message, MessageContent, SSEEvent, StatusData, ErrorData, MessageStartData, BackendCartData, Product, TextContent, ProductCardContent, ProductListContent } from '../types'
9
+ import { getUserId } from '../utils/userId'
10
+ import { useSession } from './useSession'
11
+ import { transformProducts } from '../utils/productTransformers'
12
+ import { transformCartData } from '../utils/cartTransformers'
13
+
14
+ // ============================================================================
15
+ // 辅助函数:文本解析和消息重组
16
+ // ============================================================================
17
+
18
+ /**
19
+ * 实时解析流式文本中的产品占位符
20
+ * 处理缓冲区中的文本,检测完整的 {{product:xxx}} 占位符
21
+ *
22
+ * @param buffer 当前缓冲区内容(包含新接收的文本)
23
+ * @param productMap handle → Product 的映射表
24
+ * @param rawProductMap handle → raw backend product 的映射表
25
+ * @param onAddToCart 添加到购物车回调
26
+ * @param productCardRender 自定义产品卡片渲染函数
27
+ * @returns { contents: 需要添加的内容数组, remainingBuffer: 剩余缓冲区内容 }
28
+ */
29
+ function parseStreamingText(
30
+ buffer: string,
31
+ productMap: Map<string, Product>,
32
+ rawProductMap: Map<string, any>,
33
+ onAddToCart?: (product: Product) => void,
34
+ productCardRender?: (product: any, productHandle: string) => React.ReactNode
35
+ ): { contents: MessageContent[]; remainingBuffer: string } {
36
+ const contents: MessageContent[] = []
37
+ const regex = /\{\{(?:product:)?([^}]+)\}\}/g
38
+
39
+ let lastIndex = 0
40
+ let match: RegExpExecArray | null
41
+ let foundMatch = false
42
+
43
+ // 查找所有完整的占位符
44
+ while ((match = regex.exec(buffer)) !== null) {
45
+ foundMatch = true
46
+
47
+ // 提取占位符前的文本
48
+ const beforeText = buffer.slice(lastIndex, match.index)
49
+ if (beforeText) {
50
+ contents.push({ type: 'text', text: beforeText } as TextContent)
51
+ }
52
+
53
+ // 提取产品 ID 并创建产品卡片
54
+ const productId = match[1].trim()
55
+ const product = productMap.get(productId)
56
+ const rawProduct = rawProductMap.get(productId)
57
+
58
+ // 无论是否找到产品数据,都渲染 product_card,应用层可通过 productHandle 查询产品
59
+ if (product) {
60
+ console.log('[useChatState] 🎯 实时检测到产品:', productId, '→', product.title)
61
+ } else {
62
+ console.log('[useChatState] 📦 实时检测到产品占位符,产品数据待应用层查询:', productId)
63
+ }
64
+
65
+ contents.push({
66
+ type: 'product_card',
67
+ data: {
68
+ product: product,
69
+ rawProduct: rawProduct,
70
+ productHandle: productId,
71
+ onAddToCart: onAddToCart,
72
+ productCardRender: productCardRender
73
+ }
74
+ } as ProductCardContent)
75
+
76
+ lastIndex = regex.lastIndex
77
+ }
78
+
79
+ // 如果找到了至少一个完整占位符
80
+ if (foundMatch) {
81
+ // 返回剩余的文本作为缓冲区(可能包含不完整的占位符)
82
+ const remainingBuffer = buffer.slice(lastIndex)
83
+ return { contents, remainingBuffer }
84
+ } else {
85
+ // 没有找到完整占位符,检查是否有不完整的占位符开头
86
+ // 例如:缓冲区是 "some text {{prod",我们需要保留 "{{prod" 等待更多文本
87
+ const incompleteMatch = buffer.match(/\{\{[^}]*$/)
88
+
89
+ if (incompleteMatch) {
90
+ // 有不完整的占位符开头
91
+ const completeText = buffer.slice(0, incompleteMatch.index)
92
+ if (completeText) {
93
+ contents.push({ type: 'text', text: completeText } as TextContent)
94
+ }
95
+ return { contents, remainingBuffer: incompleteMatch[0] }
96
+ } else {
97
+ // 没有不完整的占位符,整个缓冲区都是普通文本
98
+ if (buffer) {
99
+ contents.push({ type: 'text', text: buffer } as TextContent)
100
+ }
101
+ return { contents, remainingBuffer: '' }
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * 解析文本中的 {{handle}},返回 [text, product_card, text, ...] 数组(用于历史消息重组)
108
+ *
109
+ * @param text 包含 {{handle}} 标记的文本
110
+ * @param productMap handle → Product 的映射表
111
+ * @param rawProductMap handle → raw backend product 的映射表
112
+ * @param onAddToCart 添加到购物车回调
113
+ * @param productCardRender 自定义产品卡片渲染函数
114
+ * @returns MessageContent 数组
115
+ *
116
+ * @example
117
+ * 输入:
118
+ * text: "我推荐以下产品:\n{{product-handle}}\n这款产品性价比很高。"
119
+ * productMap: Map { 'product-handle' => Product {...} }
120
+ * 输出:
121
+ * [
122
+ * { type: 'text', text: '我推荐以下产品:' },
123
+ * { type: 'product_card', data: { product: {...}, onAddToCart } },
124
+ * { type: 'text', text: '这款产品性价比很高。' }
125
+ * ]
126
+ */
127
+ function parseTextWithProductIds(
128
+ text: string,
129
+ productMap: Map<string, Product>,
130
+ rawProductMap: Map<string, any>,
131
+ onAddToCart?: (product: Product) => void,
132
+ productCardRender?: (product: any, productHandle: string) => React.ReactNode
133
+ ): MessageContent[] {
134
+ const result: MessageContent[] = []
135
+ // 修改正则表达式以匹配 {{product:ID}} 格式
136
+ // 匹配 {{product:xxx}} 或 {{xxx}}(兼容两种格式)
137
+ const regex = /\{\{(?:product:)?([^}]+)\}\}/g
138
+
139
+ let lastIndex = 0
140
+ let match: RegExpExecArray | null
141
+
142
+ while ((match = regex.exec(text)) !== null) {
143
+ const beforeText = text.slice(lastIndex, match.index).trim()
144
+ // match[1] 是捕获组中的内容,即 product: 后面的 ID
145
+ const productId = match[1].trim()
146
+
147
+ // 添加前面的文本(如果有)
148
+ if (beforeText) {
149
+ result.push({
150
+ type: 'text',
151
+ text: beforeText,
152
+ } as TextContent)
153
+ }
154
+
155
+ // 添加产品卡片(无论是否找到产品数据,都渲染 product_card)
156
+ const product = productMap.get(productId)
157
+ const rawProduct = rawProductMap.get(productId)
158
+ if (product) {
159
+ console.log(`[useChatState] ✅ 找到产品匹配: ${productId} → ${product.title}`)
160
+ } else {
161
+ console.log(`[useChatState] 📦 产品占位符,产品数据待应用层查询: ${productId}`)
162
+ }
163
+
164
+ result.push({
165
+ type: 'product_card',
166
+ data: {
167
+ product: product,
168
+ rawProduct: rawProduct,
169
+ productHandle: productId,
170
+ onAddToCart: onAddToCart,
171
+ productCardRender: productCardRender,
172
+ },
173
+ } as ProductCardContent)
174
+
175
+ lastIndex = regex.lastIndex
176
+ }
177
+
178
+ // 添加最后剩余的文本
179
+ const remainingText = text.slice(lastIndex).trim()
180
+ if (remainingText) {
181
+ result.push({
182
+ type: 'text',
183
+ text: remainingText,
184
+ } as TextContent)
185
+ }
186
+
187
+ return result
188
+ }
189
+
190
+ /**
191
+ * 重组消息内容:解析文本中的 {{handle}},替换为产品卡片
192
+ *
193
+ * 处理逻辑:
194
+ * 1. 遍历消息的所有 content blocks
195
+ * 2. 对于 text 类型,解析其中的 {{handle}} 并拆分为多个 content
196
+ * 3. 跳过 product_list 类型(已经被拆分到文本中)
197
+ * 4. 其他类型直接保留
198
+ *
199
+ * @param contents 原始消息内容数组
200
+ * @param productMap handle → Product 的映射表
201
+ * @param rawProductMap handle → raw backend product 的映射表
202
+ * @param onAddToCart 添加到购物车回调
203
+ * @param productCardRender 自定义产品卡片渲染函数
204
+ * @returns 重组后的消息内容数组
205
+ */
206
+ function reorganizeMessageContent(
207
+ contents: MessageContent[],
208
+ productMap: Map<string, Product>,
209
+ rawProductMap: Map<string, any>,
210
+ onAddToCart?: (product: Product) => void,
211
+ productCardRender?: (product: any, productHandle: string) => React.ReactNode
212
+ ): MessageContent[] {
213
+ const result: MessageContent[] = []
214
+
215
+ for (const content of contents) {
216
+ // 只处理文本类型
217
+ if (content.type === 'text') {
218
+ const textContent = content as TextContent
219
+ const segments = parseTextWithProductIds(textContent.text, productMap, rawProductMap, onAddToCart, productCardRender)
220
+ result.push(...segments)
221
+ }
222
+ // 跳过 product_list(已经被拆分到文本中)
223
+ else if (content.type === 'product_list') {
224
+ continue
225
+ }
226
+ // 其他类型直接保留
227
+ else {
228
+ result.push(content)
229
+ }
230
+ }
231
+
232
+ return result
233
+ }
234
+
235
+ /**
236
+ * 处理单条消息的重组(用于历史消息加载)
237
+ * 如果消息包含带有 {{}} 的文本,则进行重组
238
+ *
239
+ * @param message 原始消息
240
+ * @param onAddToCart 添加到购物车回调
241
+ * @param productCardRender 自定义产品卡片渲染函数
242
+ * @returns 重组后的消息(如果需要重组),否则返回原消息
243
+ */
244
+ function maybeReorganizeHistoricalMessage(
245
+ message: Message,
246
+ onAddToCart?: (product: Product) => void,
247
+ productCardRender?: (product: any, productHandle: string) => React.ReactNode
248
+ ): Message {
249
+ // 检查消息是否包含带有 {{}} 的文本
250
+ const hasPlaceholder = message.content.some(
251
+ c => c.type === 'text' && /\{\{(?:product:)?[^}]+\}\}/.test((c as TextContent).text)
252
+ )
253
+ if (!hasPlaceholder) {
254
+ return message // 没有占位符,不需要重组
255
+ }
256
+
257
+ console.log('[useChatState] 检测到历史消息需要重组, 消息ID:', message.id)
258
+
259
+ // 构建产品映射 (handle → Product)
260
+ const productMap = new Map<string, Product>()
261
+ // 从 structured_content 中提取原始后端产品数据 (handle → rawProduct)
262
+ const rawProductMap = new Map<string, any>()
263
+
264
+ // 优先从 structured_content 获取原始产品数据(如果存在)
265
+ if (message.structured_content) {
266
+ message.structured_content.forEach(structuredContent => {
267
+ if (structuredContent.type === 'product_list' && Array.isArray(structuredContent.data)) {
268
+ structuredContent.data.forEach((rawProduct: any) => {
269
+ if (rawProduct && rawProduct.handle) {
270
+ rawProductMap.set(rawProduct.handle, rawProduct)
271
+ console.log('[useChatState] 从 structured_content 提取原始产品数据, handle:', rawProduct.handle)
272
+ }
273
+ })
274
+ }
275
+ })
276
+ }
277
+
278
+ // 构建转换后的产品映射(用于默认渲染)
279
+ message.content.forEach(content => {
280
+ if (content.type === 'product_list') {
281
+ const productListContent = content as ProductListContent
282
+ productListContent.data.products.forEach(product => {
283
+ if (product && product.handle) {
284
+ productMap.set(product.handle, product)
285
+ }
286
+ })
287
+ }
288
+ })
289
+
290
+ // 重组消息内容
291
+ const reorganizedContent = reorganizeMessageContent(message.content, productMap, rawProductMap, onAddToCart, productCardRender)
292
+
293
+ // 返回新消息对象
294
+ return {
295
+ ...message,
296
+ content: reorganizedContent,
297
+ }
298
+ }
299
+
300
+ export interface UseChatStateOptions {
301
+ /**
302
+ * 初始欢迎消息
303
+ */
304
+ welcomeMessage?: string
305
+
306
+ /**
307
+ * Shopify 店铺域名
308
+ */
309
+ site?: string
310
+
311
+ /**
312
+ * 受控模式:是否打开聊天窗口
313
+ * 提供此参数时,组件处于受控模式
314
+ */
315
+ open?: boolean
316
+
317
+ /**
318
+ * 受控模式:状态变化回调(必需)
319
+ * 用于同步状态到父组件
320
+ */
321
+ onOpenChange?: (open: boolean) => void
322
+
323
+ /**
324
+ * 窗口打开事件监听(可选)
325
+ * 用于埋点、日志等副作用
326
+ */
327
+ onOpen?: () => void
328
+
329
+ /**
330
+ * 窗口关闭事件监听(可选)
331
+ * 用于埋点、日志等副作用
332
+ */
333
+ onClose?: () => void
334
+
335
+ /**
336
+ * 消息发送回调
337
+ */
338
+ onMessageSend?: (message: string) => void
339
+
340
+ /**
341
+ * 错误处理回调
342
+ */
343
+ onError?: (error: Error) => void
344
+
345
+ /**
346
+ * AI 消息回调
347
+ */
348
+ /**
349
+ * AI 回复文本消息时触发
350
+ */
351
+ onTextMessage?: () => void
352
+
353
+ /**
354
+ * AI 回复商品列表卡片时触发
355
+ */
356
+ onProductList?: () => void
357
+
358
+ /**
359
+ * AI 回复促销卡片时触发
360
+ */
361
+ onPromotionList?: () => void
362
+
363
+ /**
364
+ * 商品添加到购物车回调
365
+ */
366
+ onAddToCart?: (product: any) => void
367
+
368
+ /**
369
+ * 购物车按钮点击回调
370
+ */
371
+ onCart?: (cartId: string, checkoutUrl?: string) => void
372
+
373
+ /**
374
+ * 自定义产品卡片渲染函数
375
+ * @param product 产品数据(如果在 product_list 中找到),否则为 undefined
376
+ * @param productHandle 文本占位符中的产品 ID,可用于应用层查询产品数据
377
+ */
378
+ productCardRender?: (product: any, productHandle: string) => React.ReactNode
379
+ }
380
+
381
+ export interface UseChatStateReturn {
382
+ /**
383
+ * 消息列表
384
+ */
385
+ messages: Message[]
386
+
387
+ /**
388
+ * 聊天窗口是否打开
389
+ */
390
+ isOpen: boolean
391
+
392
+ /**
393
+ * 用户 ID
394
+ */
395
+ userId: string
396
+
397
+ /**
398
+ * 会话 ID
399
+ */
400
+ sessionId: string | null
401
+
402
+ /**
403
+ * 输入框内容
404
+ */
405
+ inputValue: string
406
+
407
+ /**
408
+ * 是否正在接收流式消息
409
+ */
410
+ isStreaming: boolean
411
+
412
+ /**
413
+ * 打开聊天窗口
414
+ */
415
+ openChat: () => void
416
+
417
+ /**
418
+ * 关闭聊天窗口
419
+ */
420
+ closeChat: () => void
421
+
422
+ /**
423
+ * 切换聊天窗口状态
424
+ */
425
+ toggleChat: () => void
426
+
427
+ /**
428
+ * 设置输入框内容
429
+ */
430
+ setInputValue: (value: string) => void
431
+
432
+ /**
433
+ * 添加消息到列表
434
+ */
435
+ addMessage: (message: Message) => void
436
+
437
+ /**
438
+ * 批量设置消息列表(用于加载历史)
439
+ */
440
+ setMessages: (messages: Message[]) => void
441
+
442
+ /**
443
+ * 清空消息列表
444
+ */
445
+ clearMessages: () => void
446
+
447
+ /**
448
+ * 处理 SSE 事件
449
+ */
450
+ handleSSEEvent: (event: SSEEvent) => void
451
+
452
+ /**
453
+ * 保存会话 ID
454
+ */
455
+ saveSession: (id: string) => void
456
+
457
+ /**
458
+ * 清空会话
459
+ */
460
+ clearSession: () => void
461
+ }
462
+
463
+ /**
464
+ * 聊天状态管理 Hook
465
+ *
466
+ * 功能:
467
+ * 1. 管理消息列表(添加、清空、批量设置)
468
+ * 2. 管理窗口状态(打开、关闭、切换)
469
+ * 3. 管理输入框状态
470
+ * 4. 处理 SSE 流式消息事件
471
+ * 5. 累积流式文本内容
472
+ *
473
+ * @param options Hook 配置选项
474
+ * @returns 状态管理工具对象
475
+ */
476
+ export function useChatState(options: UseChatStateOptions = {}): UseChatStateReturn {
477
+ const {
478
+ welcomeMessage,
479
+ site,
480
+ open: controlledOpen,
481
+ onOpenChange,
482
+ onOpen,
483
+ onClose,
484
+ onMessageSend,
485
+ onError,
486
+ onTextMessage,
487
+ onProductList,
488
+ onPromotionList,
489
+ onAddToCart,
490
+ onCart,
491
+ productCardRender,
492
+ } = options
493
+
494
+ // 会话管理
495
+ const { sessionId, saveSession, clearSession } = useSession()
496
+
497
+ // 用户 ID (初始化时异步生成)
498
+ const [userId, setUserId] = useState<string>('')
499
+
500
+ // 初始化 userId
501
+ useEffect(() => {
502
+ getUserId().then(id => setUserId(id))
503
+ }, [])
504
+
505
+ // 消息列表
506
+ const [messages, setMessagesState] = useState<Message[]>(() => {
507
+ // 如果有欢迎消息,初始化时添加
508
+ if (welcomeMessage) {
509
+ return [
510
+ {
511
+ id: `welcome-${Date.now()}`,
512
+ role: 'assistant',
513
+ content: [{ type: 'text', text: welcomeMessage }],
514
+ timestamp: Date.now(),
515
+ },
516
+ ]
517
+ }
518
+ return []
519
+ })
520
+
521
+ // 聊天窗口是否打开(支持受控和非受控两种模式)
522
+ const [internalOpen, setInternalOpen] = useState(false)
523
+ const isControlled = controlledOpen !== undefined
524
+ const isOpen = isControlled ? controlledOpen : internalOpen
525
+
526
+ // 输入框内容
527
+ const [inputValue, setInputValue] = useState('')
528
+
529
+ // 是否正在接收流式消息
530
+ const [isStreaming, setIsStreaming] = useState(false)
531
+
532
+ // 当前正在累积的流式消息 (临时存储)
533
+ const currentMessageRef = useRef<Message | null>(null)
534
+
535
+ // 标记当前消息是否已触发 onTextMessage 回调(避免重复触发)
536
+ const textMessageCallbackTriggeredRef = useRef<boolean>(false)
537
+
538
+ // 产品映射缓存 (handle → Product),用于实时解析占位符
539
+ const productMapRef = useRef<Map<string, Product>>(new Map())
540
+
541
+ // 原始产品数据缓存 (handle → raw backend product),用于 productCardRender
542
+ const rawProductMapRef = useRef<Map<string, any>>(new Map())
543
+
544
+ // 文本缓冲区,用于存储未完成的文本(处理占位符跨越多个 delta 的情况)
545
+ const textBufferRef = useRef<string>('')
546
+
547
+ // 卡片缓存队列,用于存储需要延迟显示的卡片(在文本完成后显示)
548
+ const pendingCardsRef = useRef<MessageContent[]>([])
549
+
550
+ /**
551
+ * 打开聊天窗口
552
+ */
553
+ const openChat = useCallback(() => {
554
+ if (!isControlled) {
555
+ setInternalOpen(true)
556
+ }
557
+ onOpenChange?.(true)
558
+ onOpen?.()
559
+ }, [isControlled, onOpenChange, onOpen])
560
+
561
+ /**
562
+ * 关闭聊天窗口
563
+ */
564
+ const closeChat = useCallback(() => {
565
+ if (!isControlled) {
566
+ setInternalOpen(false)
567
+ }
568
+ onOpenChange?.(false)
569
+ onClose?.()
570
+ }, [isControlled, onOpenChange, onClose])
571
+
572
+ /**
573
+ * 切换聊天窗口状态
574
+ */
575
+ const toggleChat = useCallback(() => {
576
+ const newState = !isOpen
577
+ if (!isControlled) {
578
+ setInternalOpen(newState)
579
+ }
580
+ onOpenChange?.(newState)
581
+ if (newState) {
582
+ onOpen?.()
583
+ } else {
584
+ onClose?.()
585
+ }
586
+ }, [isControlled, isOpen, onOpenChange, onOpen, onClose])
587
+
588
+ /**
589
+ * 添加消息到列表
590
+ */
591
+ const addMessage = useCallback((message: Message) => {
592
+ // 防护:如果消息为 null 或 undefined,不添加
593
+ if (!message) {
594
+ console.warn('[useChatState] Attempted to add null/undefined message')
595
+ return
596
+ }
597
+ setMessagesState(prev => [...prev, message])
598
+ }, [])
599
+
600
+ /**
601
+ * 批量设置消息列表(用于加载历史)
602
+ */
603
+ const setMessages = useCallback(
604
+ (newMessages: Message[]) => {
605
+ // 防护:过滤掉 null/undefined 消息
606
+ const validMessages = newMessages.filter(msg => msg != null)
607
+ if (validMessages.length !== newMessages.length) {
608
+ console.warn('[useChatState] Filtered out null/undefined messages from batch set')
609
+ }
610
+
611
+ // 对每条历史消息进行重组(如果需要)
612
+ const reorganizedMessages = validMessages.map(msg => maybeReorganizeHistoricalMessage(msg, onAddToCart, productCardRender))
613
+
614
+ setMessagesState(reorganizedMessages)
615
+ },
616
+ [onAddToCart, productCardRender]
617
+ )
618
+
619
+ /**
620
+ * 清空消息列表
621
+ */
622
+ const clearMessages = useCallback(() => {
623
+ setMessagesState([])
624
+ }, [])
625
+
626
+ /**
627
+ * 处理 SSE 事件
628
+ * 根据事件类型进行不同的处理
629
+ */
630
+ const handleSSEEvent = useCallback(
631
+ (event: SSEEvent) => {
632
+ const { event: eventType, data } = event
633
+
634
+ switch (eventType) {
635
+ case 'message_start': {
636
+ // 开始接收新消息
637
+ setIsStreaming(true)
638
+
639
+ // 重置文本消息回调标记
640
+ textMessageCallbackTriggeredRef.current = false
641
+
642
+ // 重置文本缓冲区
643
+ textBufferRef.current = ''
644
+
645
+ // 重置卡片缓存队列
646
+ pendingCardsRef.current = []
647
+
648
+ // T039: 保存 sessionId(如果后端返回)
649
+ const messageStartData = data as MessageStartData
650
+ if (messageStartData.sessionId && messageStartData.sessionId !== sessionId) {
651
+ saveSession(messageStartData.sessionId)
652
+ }
653
+
654
+ // 检查最后一条消息是否是 thinking 消息(用户发送消息时已添加)
655
+ setMessagesState(prev => {
656
+ const lastMessage = prev[prev.length - 1]
657
+ const hasThinking =
658
+ lastMessage &&
659
+ lastMessage.role === 'assistant' &&
660
+ lastMessage.content.length === 1 &&
661
+ lastMessage.content[0].type === 'thinking'
662
+
663
+ if (hasThinking) {
664
+ // 复用已存在的 thinking 消息
665
+ currentMessageRef.current = lastMessage
666
+ return prev // 不需要添加新消息
667
+ } else {
668
+ // 没有 thinking 消息,创建新的(兼容其他场景)
669
+ const messageId = `msg-${Date.now()}`
670
+ currentMessageRef.current = {
671
+ id: messageId,
672
+ role: 'assistant',
673
+ content: [{ type: 'thinking', data: { status: 'thinking' } }],
674
+ timestamp: Date.now(),
675
+ }
676
+ return [...prev, currentMessageRef.current!]
677
+ }
678
+ })
679
+ break
680
+ }
681
+
682
+ case 'content_delta': {
683
+ // 累积流式文本内容,并实时检测产品占位符
684
+ const deltaData = data as any
685
+ const deltaText = deltaData.delta || deltaData.text || ''
686
+
687
+ if (currentMessageRef.current && deltaText) {
688
+ // 触发文本消息回调(仅触发一次)
689
+ if (!textMessageCallbackTriggeredRef.current) {
690
+ textMessageCallbackTriggeredRef.current = true
691
+ onTextMessage?.()
692
+ }
693
+
694
+ // 移除思考气泡(如果存在)
695
+ const hasThinking = currentMessageRef.current.content.some(c => c.type === 'thinking')
696
+ if (hasThinking) {
697
+ currentMessageRef.current.content = currentMessageRef.current.content.filter(c => c.type !== 'thinking')
698
+ }
699
+
700
+ // 将新文本添加到缓冲区
701
+ textBufferRef.current += deltaText
702
+
703
+ // 实时解析缓冲区中的占位符
704
+ const { contents, remainingBuffer } = parseStreamingText(
705
+ textBufferRef.current,
706
+ productMapRef.current,
707
+ rawProductMapRef.current,
708
+ onAddToCart,
709
+ productCardRender
710
+ )
711
+
712
+ // 更新缓冲区为剩余内容
713
+ textBufferRef.current = remainingBuffer
714
+
715
+ // 将解析出的内容添加到消息中
716
+ if (contents.length > 0) {
717
+ contents.forEach(content => {
718
+ const lastContent = currentMessageRef.current!.content[
719
+ currentMessageRef.current!.content.length - 1
720
+ ] as MessageContent | undefined
721
+
722
+ // 如果是文本内容且最后一个也是文本,则合并
723
+ if (content.type === 'text' && lastContent && lastContent.type === 'text') {
724
+ lastContent.text += content.text
725
+ } else {
726
+ // 否则添加新内容
727
+ currentMessageRef.current!.content.push(content)
728
+ }
729
+ })
730
+
731
+ // 更新消息列表以触发渲染
732
+ setMessagesState(prev => {
733
+ if (!currentMessageRef.current) return prev
734
+
735
+ const updated = [...prev]
736
+ const existingIndex = updated.findIndex(m => m && m.id === currentMessageRef.current!.id)
737
+
738
+ if (existingIndex >= 0) {
739
+ updated[existingIndex] = { ...currentMessageRef.current! }
740
+ } else {
741
+ updated.push({ ...currentMessageRef.current! })
742
+ }
743
+
744
+ return updated
745
+ })
746
+ }
747
+ }
748
+ break
749
+ }
750
+
751
+ case 'content_block': {
752
+ // 接收结构化内容块(商品、政策等)
753
+ // API 返回格式变更:
754
+ // 新格式: {index: number, type: string, data: {...}} <- type 在外层
755
+ // 旧格式: {index: number, data: {type: string, ...}} <- type 在 data 内
756
+ const blockData = data as any
757
+ if (currentMessageRef.current) {
758
+ // 获取 type 和 data
759
+ // 优先从外层获取 type,兼容旧格式从 data 内获取
760
+ const contentType = blockData.type || blockData.data?.type
761
+ const contentData = blockData.data
762
+
763
+ if (!contentType || !contentData) {
764
+ console.warn('[useChatState] Invalid content_block:', blockData)
765
+ break
766
+ }
767
+
768
+ // ============================================================
769
+ // 转换数据结构以匹配类型定义
770
+ // 根据后端数据结构规范(飞书文档)进行转换
771
+ // ============================================================
772
+ let messageContent: MessageContent
773
+
774
+ // ========== 1. 产品列表卡片 (Product List) ==========
775
+ // 后端格式: {type: "product_list", data: [product1, product2, ...]}
776
+ // data 直接是产品数组,不是 {products: [...]}
777
+ if (contentType === 'product_list' && Array.isArray(contentData)) {
778
+ // 触发商品列表回调
779
+ onProductList?.()
780
+
781
+ // 缓存原始后端产品数据(用于 productCardRender)
782
+ // 使用 handle 作为 key 进行匹配
783
+ contentData.forEach((rawProduct: any) => {
784
+ if (rawProduct && rawProduct.handle) {
785
+ rawProductMapRef.current.set(rawProduct.handle, rawProduct)
786
+ console.log('[useChatState] 缓存原始产品数据, handle:', rawProduct.handle)
787
+ }
788
+ })
789
+
790
+ // 使用统一的产品转换工具函数
791
+ const transformedProducts = transformProducts(contentData, site)
792
+
793
+ // 建立产品映射缓存 (handle → Product)
794
+ // 用于后续在 message_end 时解析文本中的 {{handle}}
795
+ transformedProducts.forEach(product => {
796
+ if (product && product.handle) {
797
+ productMapRef.current.set(product.handle, product)
798
+
799
+ console.log('[useChatState] 建立产品映射:', {
800
+ handle: product.handle,
801
+ title: product.title,
802
+ })
803
+ }
804
+ })
805
+
806
+ // ⚠️ 不要把 product_list 添加到消息中,避免闪烁
807
+ // 等到 message_end 时,通过文本解析创建 product_card,直接显示最终的交替格式
808
+ console.log('[useChatState] ✅ 产品列表已缓存,不添加到消息中(避免闪烁)')
809
+ break // 直接跳出,不执行后续的 push 操作
810
+ }
811
+ // ========== 2. 产品对比卡片 (Product Comparison) ==========
812
+ // 后端格式: {type: "product_comparison", data: {products: [...], dimensions: {...}}}
813
+ else if (contentType === 'product_comparison' && contentData.products) {
814
+ // 使用统一的产品转换工具函数
815
+ const transformedProducts = transformProducts(contentData.products, site)
816
+
817
+ messageContent = {
818
+ type: 'product_comparison',
819
+ data: {
820
+ products: transformedProducts,
821
+ dimensions: contentData.dimensions || {},
822
+ },
823
+ } as MessageContent
824
+ }
825
+ // ========== 3. FAQ 列表卡片 (FAQ List) ==========
826
+ // 后端格式: {type: "faq_list", data: {found, count, total, results: [...]}}
827
+ else if (contentType === 'faq_list' && contentData.found !== undefined) {
828
+ messageContent = {
829
+ type: 'faq_list',
830
+ data: contentData, // 直接使用,结构已匹配
831
+ } as MessageContent
832
+ }
833
+ // ========== 4. 快捷回复 (Quick Replies) ==========
834
+ else if (contentType === 'quick_replies' && contentData.replies) {
835
+ messageContent = {
836
+ type: 'quick_replies',
837
+ data: {
838
+ replies: contentData.replies,
839
+ },
840
+ } as MessageContent
841
+ }
842
+ // ========== 5. 政策内容 (Policy) ==========
843
+ else if (contentType === 'policy' && contentData.title && contentData.content) {
844
+ messageContent = {
845
+ type: 'policy',
846
+ data: {
847
+ title: contentData.title,
848
+ content: contentData.content,
849
+ },
850
+ } as MessageContent
851
+ }
852
+ // ========== 6. 促销活动列表 (Promotion List) ==========
853
+ // 后端格式: {type: "promotion_list", data: {found, count, total, results: [...]}}
854
+ else if (contentType === 'promotion_list' && contentData.found !== undefined) {
855
+ // 触发促销卡片回调
856
+ onPromotionList?.()
857
+
858
+ messageContent = {
859
+ type: 'promotion_list',
860
+ data: contentData, // 直接使用,结构已匹配
861
+ } as MessageContent
862
+ }
863
+ // ========== 7. 购物车 (Cart) ==========
864
+ // 后端格式: {type: "cart", data: {id, lines: {edges: [...]}, cost, ...}} (Shopify GraphQL)
865
+ // 需要转换为前端格式: {cartId, lines: [...], cost, ...}
866
+ else if (contentType === 'cart' && contentData.id !== undefined) {
867
+ // 转换后端 Shopify GraphQL 格式为前端标准格式
868
+ const transformedData = transformCartData(contentData as BackendCartData)
869
+ messageContent = {
870
+ type: 'cart',
871
+ data: {
872
+ ...transformedData,
873
+ onCart: onCart, // 注入购物车按钮回调
874
+ },
875
+ } as MessageContent
876
+ }
877
+ // ========== 8. 其他类型(通用处理) ==========
878
+ else {
879
+ messageContent = {
880
+ type: contentType,
881
+ data: contentData,
882
+ } as MessageContent
883
+ }
884
+
885
+ // ⚠️ 重要修改:将卡片缓存起来,不立即添加到消息中
886
+ // 等待 message_end 时,在文本完成后再统一添加卡片
887
+ console.log('[useChatState] ✅ 卡片已缓存,等待文本完成后显示:', contentType)
888
+ pendingCardsRef.current.push(messageContent)
889
+
890
+ // 不再立即更新消息列表,避免卡片在文本之前显示
891
+ // 原来的代码:
892
+ // currentMessageRef.current.content.push(messageContent)
893
+ // setMessagesState(prev => { ... })
894
+ }
895
+ break
896
+ }
897
+
898
+ case 'tool_start':
899
+ case 'tool_end': {
900
+ // 工具调用事件,暂时忽略
901
+ // 可以在未来用于显示工具调用状态
902
+ break
903
+ }
904
+
905
+ case 'message_end': {
906
+ // 消息接收完成
907
+ setIsStreaming(false)
908
+
909
+ // 处理缓冲区中剩余的文本(如果有)
910
+ if (currentMessageRef.current && textBufferRef.current) {
911
+ console.log('[useChatState] 处理剩余缓冲区:', textBufferRef.current)
912
+
913
+ const lastContent = currentMessageRef.current.content[
914
+ currentMessageRef.current.content.length - 1
915
+ ] as MessageContent | undefined
916
+
917
+ // 如果最后一个内容是文本,则合并
918
+ if (lastContent && lastContent.type === 'text') {
919
+ lastContent.text += textBufferRef.current
920
+ } else {
921
+ // 否则添加为新的文本块
922
+ currentMessageRef.current.content.push({
923
+ type: 'text',
924
+ text: textBufferRef.current,
925
+ })
926
+ }
927
+ }
928
+
929
+ // ⚠️ 重要修改:在文本完成后,添加所有缓存的卡片
930
+ if (currentMessageRef.current && pendingCardsRef.current.length > 0) {
931
+ console.log('[useChatState] 📋 文本已完成,现在添加', pendingCardsRef.current.length, '个缓存的卡片')
932
+
933
+ // 将所有缓存的卡片添加到消息内容中
934
+ currentMessageRef.current.content.push(...pendingCardsRef.current)
935
+ }
936
+
937
+ // ⚠️ 超时检测:如果 message_end 时仍存在 thinking block,视为超时/异常
938
+ // 注意:在添加卡片之后检测,这样如果有卡片就不会添加超时提示
939
+ if (currentMessageRef.current) {
940
+ const hasThinking = currentMessageRef.current.content.some(c => c.type === 'thinking')
941
+
942
+ if (hasThinking) {
943
+ console.log('[useChatState] ⚠️ message_end 时仍存在 thinking block,立即移除(视为超时)')
944
+
945
+ // 移除 thinking block
946
+ currentMessageRef.current.content = currentMessageRef.current.content.filter(c => c.type !== 'thinking')
947
+
948
+ // 如果没有其他内容(包括缓存的卡片),添加超时提示
949
+ if (currentMessageRef.current.content.length === 0) {
950
+ currentMessageRef.current.content.push({
951
+ type: 'text',
952
+ text: 'Response timed out, please try again.',
953
+ } as TextContent)
954
+ }
955
+ }
956
+ }
957
+
958
+ // 更新消息列表(统一更新,包含文本和卡片)
959
+ if (currentMessageRef.current) {
960
+ setMessagesState(prev => {
961
+ if (!currentMessageRef.current) return prev
962
+
963
+ const updated = [...prev]
964
+ const existingIndex = updated.findIndex(m => m && m.id === currentMessageRef.current!.id)
965
+
966
+ if (existingIndex >= 0) {
967
+ updated[existingIndex] = { ...currentMessageRef.current! }
968
+ }
969
+
970
+ return updated
971
+ })
972
+ }
973
+
974
+ // 清空缓冲区、产品映射和卡片缓存
975
+ textBufferRef.current = ''
976
+ pendingCardsRef.current = []
977
+ productMapRef.current.clear()
978
+ rawProductMapRef.current.clear()
979
+ currentMessageRef.current = null
980
+ break
981
+ }
982
+
983
+ case 'status': {
984
+ // T040: 状态更新(如会话过期)
985
+ const statusData = data as StatusData
986
+ if (statusData.type === 'session_expired') {
987
+ // 会话过期,清空消息列表和会话
988
+ clearMessages()
989
+ clearSession()
990
+ if (welcomeMessage) {
991
+ addMessage({
992
+ id: `welcome-${Date.now()}`,
993
+ role: 'assistant',
994
+ content: [{ type: 'text', text: welcomeMessage }],
995
+ timestamp: Date.now(),
996
+ })
997
+ }
998
+ }
999
+ break
1000
+ }
1001
+
1002
+ case 'error': {
1003
+ // 错误处理
1004
+ const errorData = data as ErrorData
1005
+ setIsStreaming(false)
1006
+
1007
+ // 清理缓存(防止泄漏到下次消息)
1008
+ textBufferRef.current = ''
1009
+ pendingCardsRef.current = []
1010
+ productMapRef.current.clear()
1011
+ rawProductMapRef.current.clear()
1012
+ currentMessageRef.current = null
1013
+
1014
+ // 添加错误消息到界面
1015
+ addMessage({
1016
+ id: `error-${Date.now()}`,
1017
+ role: 'system',
1018
+ content: [
1019
+ {
1020
+ type: 'error',
1021
+ data: {
1022
+ message: errorData.message,
1023
+ code: errorData.code,
1024
+ },
1025
+ },
1026
+ ],
1027
+ timestamp: Date.now(),
1028
+ })
1029
+
1030
+ onError?.(new Error(errorData.message))
1031
+ break
1032
+ }
1033
+
1034
+ case 'done': {
1035
+ // 流结束
1036
+ setIsStreaming(false)
1037
+
1038
+ // 清理缓存(防止泄漏到下次消息)
1039
+ textBufferRef.current = ''
1040
+ pendingCardsRef.current = []
1041
+ productMapRef.current.clear()
1042
+ rawProductMapRef.current.clear()
1043
+
1044
+ // 清理当前消息引用
1045
+ currentMessageRef.current = null
1046
+ break
1047
+ }
1048
+
1049
+ default:
1050
+ // 其他事件类型(tool_start, tool_end 等)
1051
+ break
1052
+ }
1053
+ },
1054
+ [
1055
+ welcomeMessage,
1056
+ site,
1057
+ addMessage,
1058
+ clearMessages,
1059
+ clearSession,
1060
+ saveSession,
1061
+ sessionId,
1062
+ onError,
1063
+ onTextMessage,
1064
+ onProductList,
1065
+ onPromotionList,
1066
+ onAddToCart,
1067
+ onCart,
1068
+ ]
1069
+ )
1070
+
1071
+
1072
+ return {
1073
+ messages,
1074
+ isOpen,
1075
+ userId,
1076
+ sessionId,
1077
+ inputValue,
1078
+ isStreaming,
1079
+ openChat,
1080
+ closeChat,
1081
+ toggleChat,
1082
+ setInputValue,
1083
+ addMessage,
1084
+ setMessages,
1085
+ clearMessages,
1086
+ handleSSEEvent,
1087
+ saveSession,
1088
+ clearSession,
1089
+ }
1090
+ }